Lua runner with rich builtin stdlib
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
a/src/lua_tests/table_tests.rs

1497 lines
41 KiB

//! Tests for the `table` stdlib extensions (lua/stdlib/table.lua).
//! Ported from flowbox-rt's fb_lua lua_tests/table_tests.rs.
use super::assert_eq_f64;
use super::json_eq;
#[test]
fn test_readonly_lua_function() {
let lua = super::lua();
// Test that reads work on readonly table
let result: i64 = lua
.load(
r#"
local t = {foo = 42, bar = "hello"}
local ro = table.readonly(t)
return ro.foo
"#,
)
.eval()
.unwrap();
assert_eq!(result, 42);
// Test that writes fail on readonly table
let write_result = lua.load(
r#"
local t = {foo = 42}
local ro = table.readonly(t)
ro.foo = 100
"#,
);
let err = write_result.exec().unwrap_err();
assert!(err.to_string().contains("read-only table"));
// Test adding new keys also fails
let add_result = lua.load(
r#"
local t = {foo = 42}
local ro = table.readonly(t)
ro.new_key = "value"
"#,
);
let err = add_result.exec().unwrap_err();
assert!(err.to_string().contains("read-only table"));
}
#[test]
fn test_readonly_with_name() {
let lua = super::lua();
// Reads still pass through when a name is given
let val: i64 = lua
.load(
r#"
local ro = table.readonly({x = 1}, "MyTable")
return ro.x
"#,
)
.eval()
.unwrap();
assert_eq!(val, 1);
// The name is included in the write error message
let err = lua
.load(
r#"
local ro = table.readonly({x = 1}, "MyTable")
ro.x = 2
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("read-only table MyTable"));
}
#[test]
fn test_readonly_metatable_protected() {
let lua = super::lua();
// The metatable must not be reachable - otherwise scripts could mutate
// the protected table through getmetatable(ro).__index
let result: bool = lua
.load(
r#"
local ro = table.readonly({x = 1})
return getmetatable(ro) == false
"#,
)
.eval()
.unwrap();
assert!(result);
// Replacing the metatable must fail too
let err = lua
.load(
r#"
local ro = table.readonly({x = 1})
setmetatable(ro, {})
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("protected metatable"));
}
#[test]
fn test_merge_tables() {
let lua = super::lua();
// Merge two tables with string keys (second overwrites first)
let result: String = lua
.load(
r#"
local a = {x = 1, y = 2}
local b = {y = 10, z = 3}
local merged = table.merge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":1,"y":10,"z":3}"#));
// Merge array-style tables (numeric keys append)
let result: String = lua
.load(
r#"
local a = {1, 2, 3}
local b = {4, 5}
local merged = table.merge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3,4,5]");
// Merge more than two tables
let result: String = lua
.load(
r#"
local a = {x = 1}
local b = {y = 2}
local c = {z = 3}
local merged = table.merge(a, b, c)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":1,"y":2,"z":3}"#));
// Empty table handling
let result: String = lua
.load(
r#"
local a = {x = 42}
local merged = table.merge({}, a, {})
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":42}"#));
// Original tables are not modified
let result: String = lua
.load(
r#"
local a = {x = 1}
local b = {x = 2}
local _ = table.merge(a, b)
return utils.toJSON(a)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":1}"#));
// Nested tables are NOT recursively merged (shallow merge)
let result: String = lua
.load(
r#"
local a = {config = {a = 1, b = 2}}
local b = {config = {c = 3}}
local merged = table.merge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
// config is overwritten entirely, not merged
assert!(json_eq(&result, r#"{"config":{"c":3}}"#));
}
#[test]
fn test_deep_merge_tables() {
let lua = super::lua();
// Simple merge (same as table.merge for flat tables)
let result: String = lua
.load(
r#"
local a = {x = 1, y = 2}
local b = {y = 10, z = 3}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":1,"y":10,"z":3}"#));
// Deep merge of nested tables
let result: String = lua
.load(
r#"
local a = {config = {a = 1, b = 2}}
local b = {config = {b = 20, c = 3}}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
// config is merged recursively
assert!(json_eq(&result, r#"{"config":{"a":1,"b":20,"c":3}}"#));
// Multiple levels of nesting
let result: String = lua
.load(
r#"
local a = {a = {b = {c = {val = 1}}}}
local b = {a = {b = {c = {other = 2}, d = 3}}}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"a":{"b":{"c":{"other":2,"val":1},"d":3}}}"#));
// Table overwrites non-table
let result: String = lua
.load(
r#"
local a = {x = 1}
local b = {x = {nested = 42}}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":{"nested":42}}"#));
// Non-table overwrites table
let result: String = lua
.load(
r#"
local a = {x = {nested = 42}}
local b = {x = 100}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":100}"#));
// Merge more than two tables with nesting
let result: String = lua
.load(
r#"
local a = {config = {a = 1}}
local b = {config = {b = 2}}
local c = {config = {c = 3}}
local merged = table.deepMerge(a, b, c)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"config":{"a":1,"b":2,"c":3}}"#));
// Array-style tables append (same as table.merge)
let result: String = lua
.load(
r#"
local a = {1, 2}
local b = {3, 4}
local merged = table.deepMerge(a, b)
return utils.toJSON(merged)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3,4]");
// Original tables are not modified
let result: String = lua
.load(
r#"
local a = {config = {val = 1}}
local b = {config = {val = 2}}
local _ = table.deepMerge(a, b)
return utils.toJSON(a)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"config":{"val":1}}"#));
// Recursion limit protection (should error at depth > 100)
let err = lua
.load(
r#"
local function buildDeep(depth)
if depth == 0 then
return {val = 1}
end
return {nested = buildDeep(depth - 1)}
end
local a = buildDeep(101)
local b = buildDeep(101)
return table.deepMerge(a, b)
"#,
)
.exec();
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("recursion too deep"));
}
#[test]
fn test_filter() {
let lua = super::lua();
// Filter array with predicate - preserves sequential numeric keys, serializes as array
let result: String = lua
.load(
r#"
local t = {1, 2, 3, 4, 5, 6}
local filtered = table.filter(t, function(v) return v <= 3 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3]");
// Filter dictionary with predicate
let result: String = lua
.load(
r#"
local t = {a = 10, b = 20, c = 30, d = 5}
local filtered = table.filter(t, function(v) return v >= 15 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"b":20,"c":30}"#));
// Filter with empty result
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local filtered = table.filter(t, function(v) return v > 100 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// Filter empty table
let result: String = lua
.load(
r#"
local t = {}
local filtered = table.filter(t, function(v) return true end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// Original table unmodified
let result: String = lua
.load(
r#"
local t = {a = 1, b = 2, c = 3}
local _ = table.filter(t, function(v) return v == 2 end)
return utils.toJSON(t)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"a":1,"b":2,"c":3}"#));
}
#[test]
fn test_ifilter() {
let lua = super::lua();
// ifilter produces numbered table without gaps
let result: String = lua
.load(
r#"
local t = {1, 2, 3, 4, 5, 6}
local filtered = table.ifilter(t, function(v) return v % 2 == 0 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
// Should be a clean array [2, 4, 6] without gaps
assert_eq!(result, "[2,4,6]");
// ifilter with none matching
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local filtered = table.ifilter(t, function(v) return v > 100 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// ifilter with all matching
let result: String = lua
.load(
r#"
local t = {10, 20, 30}
local filtered = table.ifilter(t, function(v) return v >= 5 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[10,20,30]");
// ifilter preserves order
let result: String = lua
.load(
r#"
local t = {"a", "bb", "ccc", "dddd", "eeeee"}
local filtered = table.ifilter(t, function(v) return #v >= 3 end)
return utils.toJSON(filtered)
"#,
)
.eval()
.unwrap();
assert_eq!(result, r#"["ccc","dddd","eeeee"]"#);
// Original table unmodified
let result: String = lua
.load(
r#"
local t = {1, 2, 3, 4, 5}
local _ = table.ifilter(t, function(v) return v == 3 end)
return utils.toJSON(t)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3,4,5]");
}
#[test]
fn test_map() {
let lua = super::lua();
// Map array values
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local mapped = table.map(t, function(v) return v * 2 end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[2,4,6]");
// Map dictionary values
let result: String = lua
.load(
r#"
local t = {a = 1, b = 2, c = 3}
local mapped = table.map(t, function(v) return v + 10 end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"a":11,"b":12,"c":13}"#));
// Map to different types
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local mapped = table.map(t, function(v) return "val" .. v end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, r#"["val1","val2","val3"]"#);
// Map empty table
let result: String = lua
.load(
r#"
local t = {}
local mapped = table.map(t, function(v) return v * 2 end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// Original table unmodified
let result: String = lua
.load(
r#"
local t = {x = 100}
local _ = table.map(t, function(v) return v * 999 end)
return utils.toJSON(t)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":100}"#));
}
#[test]
fn test_filter_map() {
let lua = super::lua();
// filterMap drops nil results
let result: String = lua
.load(
r#"
local t = {a = 1, b = 2, c = 3, d = 4}
local mapped = table.filterMap(t, function(v)
if v % 2 == 0 then
return v * 10
else
return nil
end
end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"b":20,"d":40}"#));
// filterMap all pass (none return nil)
let result: String = lua
.load(
r#"
local t = {x = 5, y = 10}
local mapped = table.filterMap(t, function(v) return v + 1 end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&result, r#"{"x":6,"y":11}"#));
// filterMap all filtered (all return nil)
let result: String = lua
.load(
r#"
local t = {a = 1, b = 2}
local mapped = table.filterMap(t, function(v) return nil end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// filterMap on numbered table may leave gaps (preserves keys)
let result: String = lua
.load(
r#"
local t = {10, 20, 30, 40, 50}
local mapped = table.filterMap(t, function(v)
if v > 20 then return v end
return nil
end)
-- Check that the result has correct values even if keys are sparse
local count = 0
local sum = 0
for _, v in pairs(mapped) do
count = count + 1
sum = sum + v
end
return count .. ":" .. sum
"#,
)
.eval()
.unwrap();
// Should have 3 values: 30, 40, 50 summing to 120
assert_eq!(result, "3:120");
}
#[test]
fn test_ifilter_map() {
let lua = super::lua();
// ifilterMap produces numbered table without gaps
let result: String = lua
.load(
r#"
local t = {1, 2, 3, 4, 5}
local mapped = table.ifilterMap(t, function(v)
if v % 2 == 1 then
return v * 100
end
return nil
end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
// Should produce clean array [100, 300, 500]
assert_eq!(result, "[100,300,500]");
// ifilterMap preserves order
let result: String = lua
.load(
r#"
local t = {"apple", "banana", "cherry", "date"}
local mapped = table.ifilterMap(t, function(v)
if #v > 5 then
return string.upper(v)
end
return nil
end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, r#"["BANANA","CHERRY"]"#);
// ifilterMap empty result
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local mapped = table.ifilterMap(t, function(v) return nil end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "{}");
// ifilterMap all values pass
let result: String = lua
.load(
r#"
local t = {10, 20, 30}
local mapped = table.ifilterMap(t, function(v) return v // 10 end)
return utils.toJSON(mapped)
"#,
)
.eval()
.unwrap();
// Using integer division (//) to get integers
assert_eq!(result, "[1,2,3]");
}
#[test]
fn test_reduce() {
let lua = super::lua();
// Sum of values
let result: i64 = lua
.load(
r#"
local t = {1, 2, 3, 4, 5}
return table.reduce(t, function(acc, v) return acc + v end, 0)
"#,
)
.eval()
.unwrap();
assert_eq!(result, 15);
// Product of values
let result: i64 = lua
.load(
r#"
local t = {2, 3, 4}
return table.reduce(t, function(acc, v) return acc * v end, 1)
"#,
)
.eval()
.unwrap();
assert_eq!(result, 24);
// String concatenation
let result: String = lua
.load(
r#"
local t = {"hello", " ", "world"}
return table.reduce(t, function(acc, v) return acc .. v end, "")
"#,
)
.eval()
.unwrap();
assert_eq!(result, "hello world");
// Reduce empty table returns initial value
let result: i64 = lua
.load(
r#"
local t = {}
return table.reduce(t, function(acc, v) return acc + v end, 42)
"#,
)
.eval()
.unwrap();
assert_eq!(result, 42);
// Reduce dictionary values (order unspecified but result should be deterministic)
let result: i64 = lua
.load(
r#"
local t = {a = 10, b = 20, c = 30}
return table.reduce(t, function(acc, v) return acc + v end, 0)
"#,
)
.eval()
.unwrap();
assert_eq!(result, 60);
// Reduce with complex accumulator (count and sum)
let result: String = lua
.load(
r#"
local t = {5, 10, 15, 20}
local r = table.reduce(t, function(acc, v)
acc.count = acc.count + 1
acc.sum = acc.sum + v
return acc
end, {count = 0, sum = 0})
return r.count .. ":" .. r.sum
"#,
)
.eval()
.unwrap();
assert_eq!(result, "4:50");
}
#[test]
fn test_min() {
let lua = super::lua();
// Min of positive numbers
let result: i64 = lua.load(r#"return table.min({5, 3, 8, 1, 9})"#).eval().unwrap();
assert_eq!(result, 1);
// Min with negative numbers
let result: i64 = lua.load(r#"return table.min({-5, 3, -8, 1, 9})"#).eval().unwrap();
assert_eq!(result, -8);
// Min of floats
let result: f64 = lua.load(r#"return table.min({3.14, 2.71, 1.41})"#).eval().unwrap();
assert_eq_f64!(result, 1.41);
// Min with single element
let result: i64 = lua.load(r#"return table.min({42})"#).eval().unwrap();
assert_eq!(result, 42);
// Min of empty table returns nil
let result: bool = lua.load(r#"return table.min({}) == nil"#).eval().unwrap();
assert!(result);
// Min works on dictionary values too (uses pairs)
let result: i64 = lua.load(r#"return table.min({a = 100, b = 50, c = 75})"#).eval().unwrap();
assert_eq!(result, 50);
}
#[test]
fn test_max() {
let lua = super::lua();
// Max of positive numbers
let result: i64 = lua.load(r#"return table.max({5, 3, 8, 1, 9})"#).eval().unwrap();
assert_eq!(result, 9);
// Max with negative numbers
let result: i64 = lua.load(r#"return table.max({-5, -3, -8, -1, -9})"#).eval().unwrap();
assert_eq!(result, -1);
// Max of floats
let result: f64 = lua.load(r#"return table.max({3.14, 2.71, 1.41})"#).eval().unwrap();
assert_eq_f64!(result, 3.14);
// Max with single element
let result: i64 = lua.load(r#"return table.max({42})"#).eval().unwrap();
assert_eq!(result, 42);
// Max of empty table returns nil
let result: bool = lua.load(r#"return table.max({}) == nil"#).eval().unwrap();
assert!(result);
// Max works on dictionary values too
let result: i64 = lua.load(r#"return table.max({a = 100, b = 50, c = 75})"#).eval().unwrap();
assert_eq!(result, 100);
}
#[test]
fn test_mean() {
let lua = super::lua();
// Mean of integers
let result: f64 = lua.load(r#"return table.mean({10, 20, 30})"#).eval().unwrap();
assert_eq_f64!(result, 20.0);
// Mean of floats: (1.5 + 2.5 + 3.0) / 3 = 7.0 / 3
let result: f64 = lua.load(r#"return table.mean({1.5, 2.5, 3.0})"#).eval().unwrap();
assert_eq_f64!(result, 7.0 / 3.0);
// Mean with single element
let result: f64 = lua.load(r#"return table.mean({42})"#).eval().unwrap();
assert_eq_f64!(result, 42.0);
// Mean of empty table returns nil (consistent with min/max)
let result: bool = lua.load(r#"return table.mean({}) == nil"#).eval().unwrap();
assert!(result);
// Mean with negative numbers
let result: f64 = lua.load(r#"return table.mean({-10, 0, 10})"#).eval().unwrap();
assert_eq_f64!(result, 0.0);
// Mean works on dictionary values too
let result: f64 = lua.load(r#"return table.mean({a = 100, b = 200, c = 300})"#).eval().unwrap();
assert_eq_f64!(result, 200.0);
}
#[test]
fn test_sum() {
let lua = super::lua();
// Sum of integers
let result: i64 = lua.load(r#"return table.sum({1, 2, 3, 4, 5})"#).eval().unwrap();
assert_eq!(result, 15);
// Sum of floats
let result: f64 = lua.load(r#"return table.sum({1.5, 2.5, 3.0})"#).eval().unwrap();
assert_eq_f64!(result, 7.0);
// Sum with negative numbers
let result: i64 = lua.load(r#"return table.sum({-10, 5, -5, 10})"#).eval().unwrap();
assert_eq!(result, 0);
// Sum of empty table returns 0 (unlike min/max/mean which return nil)
let result: i64 = lua.load(r#"return table.sum({})"#).eval().unwrap();
assert_eq!(result, 0);
// The 0 result of an empty sum is an integer
let result: String = lua.load(r#"return math.type(table.sum({}))"#).eval().unwrap();
assert_eq!(result, "integer");
// Sum works on dictionary values
let result: i64 = lua.load(r#"return table.sum({a = 10, b = 20, c = 30})"#).eval().unwrap();
assert_eq!(result, 60);
// Sum with single element
let result: i64 = lua.load(r#"return table.sum({42})"#).eval().unwrap();
assert_eq!(result, 42);
}
#[test]
fn test_keys() {
let lua = super::lua();
// Keys of a dictionary - order unspecified, so check via sorting
let result: String = lua
.load(
r#"
local k = table.keys({a = 1, b = 2, c = 3})
table.sort(k)
return utils.toJSON(k)
"#,
)
.eval()
.unwrap();
assert_eq!(result, r#"["a","b","c"]"#);
// Keys of an array are numeric indices
let result: String = lua
.load(
r#"
local k = table.keys({"x", "y", "z"})
table.sort(k)
return utils.toJSON(k)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3]");
// Keys of empty table
let result: String = lua.load(r#"return utils.toJSON(table.keys({}))"#).eval().unwrap();
assert_eq!(result, "{}");
// Keys count matches table size
let result: i64 = lua.load(r#"return #table.keys({a = 1, b = 2, c = 3, d = 4})"#).eval().unwrap();
assert_eq!(result, 4);
}
#[test]
fn test_values() {
let lua = super::lua();
// Values of a dictionary - order unspecified, so check via sorting
let result: String = lua
.load(
r#"
local v = table.values({a = 10, b = 20, c = 30})
table.sort(v)
return utils.toJSON(v)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[10,20,30]");
// Values of an array
let result: String = lua
.load(
r#"
local v = table.values({"x", "y", "z"})
table.sort(v)
return utils.toJSON(v)
"#,
)
.eval()
.unwrap();
assert_eq!(result, r#"["x","y","z"]"#);
// Values of empty table
let result: String = lua.load(r#"return utils.toJSON(table.values({}))"#).eval().unwrap();
assert_eq!(result, "{}");
// Values count matches table size
let result: i64 = lua
.load(r#"return #table.values({a = 1, b = 2, c = 3, d = 4})"#)
.eval()
.unwrap();
assert_eq!(result, 4);
}
#[test]
fn test_contains() {
let lua = super::lua();
// Contains in array
let result: bool = lua.load(r#"return table.contains({1, 2, 3, 4, 5}, 3)"#).eval().unwrap();
assert!(result);
// Does not contain
let result: bool = lua.load(r#"return table.contains({1, 2, 3, 4, 5}, 99)"#).eval().unwrap();
assert!(!result);
// Contains in dictionary values
let result: bool = lua
.load(r#"return table.contains({a = "foo", b = "bar"}, "bar")"#)
.eval()
.unwrap();
assert!(result);
// Does not check keys, only values
let result: bool = lua.load(r#"return table.contains({a = 1, b = 2}, "a")"#).eval().unwrap();
assert!(!result);
// Contains nil check (nil is never contained)
let result: bool = lua.load(r#"return table.contains({1, nil, 3}, nil)"#).eval().unwrap();
assert!(!result);
// Empty table contains nothing
let result: bool = lua.load(r#"return table.contains({}, 1)"#).eval().unwrap();
assert!(!result);
// Contains with string values
let result: bool = lua
.load(r#"return table.contains({"apple", "banana", "cherry"}, "banana")"#)
.eval()
.unwrap();
assert!(result);
}
#[test]
fn test_is_empty() {
let lua = super::lua();
// Empty table is empty
let result: bool = lua.load(r#"return table.isEmpty({})"#).eval().unwrap();
assert!(result);
// Array is not empty
let result: bool = lua.load(r#"return table.isEmpty({1, 2, 3})"#).eval().unwrap();
assert!(!result);
// Dictionary is not empty
let result: bool = lua.load(r#"return table.isEmpty({a = 1})"#).eval().unwrap();
assert!(!result);
// Table with only nil values is empty (nil doesn't count as a value)
let result: bool = lua.load(r#"return table.isEmpty({nil, nil})"#).eval().unwrap();
assert!(result);
// Single element is not empty
let result: bool = lua.load(r#"return table.isEmpty({42})"#).eval().unwrap();
assert!(!result);
}
#[test]
fn test_min_max_nan_propagates() {
let lua = super::lua();
// A NaN anywhere in the input must yield NaN, not an arbitrary survivor value
let result: bool = lua.load(r#"local r = table.max({5, 0/0, 3}); return r ~= r"#).eval().unwrap();
assert!(result, "max with NaN in the middle must be NaN");
let result: bool = lua.load(r#"local r = table.min({3, 0/0, 5}); return r ~= r"#).eval().unwrap();
assert!(result, "min with NaN in the middle must be NaN");
let result: bool = lua.load(r#"local r = table.max({0/0, 5}); return r ~= r"#).eval().unwrap();
assert!(result, "max with NaN first must be NaN");
let result: bool = lua.load(r#"local r = table.min({0/0}); return r ~= r"#).eval().unwrap();
assert!(result, "min of only NaN must be NaN");
}
#[test]
fn test_merge_argument_validation() {
let lua = super::lua();
// A nil argument must error, not silently swallow the remaining arguments
let err = lua
.load(
r#"
local function maybeNil() return nil end
return table.merge({a = 1}, maybeNil(), {b = 2})
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("not a table"));
// Non-table arguments must error with a clear message
let err = lua.load(r#"return table.merge({}, 5)"#).exec().unwrap_err();
assert!(err.to_string().contains("not a table"));
// Same for deepMerge
let err = lua.load(r#"return table.deepMerge({a = 1}, nil, {b = 2})"#).exec().unwrap_err();
assert!(err.to_string().contains("not a table"));
// No arguments is a valid (empty) merge
let result: bool = lua.load(r#"return table.isEmpty(table.merge())"#).eval().unwrap();
assert!(result);
}
#[test]
fn test_merge_key_handling() {
let lua = super::lua();
// Non-sequence numeric keys (float, zero, negative, sparse) are preserved, not renumbered
let result: bool = lua
.load(
r#"
local m = table.merge({[1.5] = "x", [0] = "y", [-3] = "z"})
return m[1.5] == "x" and m[0] == "y" and m[-3] == "z" and m[1] == nil
"#,
)
.eval()
.unwrap();
assert!(result);
// Sparse numeric keys beyond the sequence stay where they are
let result: bool = lua
.load(
r#"
local m = table.merge({[1] = "a", [5] = "b"})
return m[1] == "a" and m[5] == "b" and m[2] == nil
"#,
)
.eval()
.unwrap();
assert!(result);
// Non-sequence numeric keys collide map-style: later table overwrites
let result: String = lua
.load(r#"return table.merge({[10] = "a"}, {[10] = "b"})[10]"#)
.eval()
.unwrap();
assert_eq!(result, "b");
// The sequence parts still concatenate in order
let result: String = lua
.load(r#"return utils.toJSON(table.merge({1, 2}, {3, 4}, {5}))"#)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3,4,5]");
}
#[test]
fn test_deep_merge_array_concat() {
let lua = super::lua();
// Arrays of tables concatenate just like arrays of scalars -
// element-wise merging only applies to non-sequence keys
let result: bool = lua
.load(
r#"
local m = table.deepMerge({{a = 1}}, {{b = 2}})
return #m == 2 and m[1].a == 1 and m[1].b == nil and m[2].b == 2 and m[2].a == nil
"#,
)
.eval()
.unwrap();
assert!(result);
}
#[test]
fn test_deep_merge_no_aliasing() {
let lua = super::lua();
// Mutating the merge result must never touch the input tables
let result: i64 = lua
.load(
r#"
local inner = {val = 1}
local m = table.deepMerge({}, {x = inner})
m.x.val = 99
return inner.val
"#,
)
.eval()
.unwrap();
assert_eq!(result, 1);
// Also for appended sequence elements
let result: i64 = lua
.load(
r#"
local inner = {val = 1}
local m = table.deepMerge({inner})
m[1].val = 99
return inner.val
"#,
)
.eval()
.unwrap();
assert_eq!(result, 1);
// And for nested tables adopted deep inside
let result: i64 = lua
.load(
r#"
local deep = {leaf = 1}
local m = table.deepMerge({cfg = {}}, {cfg = {sub = deep}})
m.cfg.sub.leaf = 99
return deep.leaf
"#,
)
.eval()
.unwrap();
assert_eq!(result, 1);
}
#[test]
fn test_deep_merge_error_message() {
let lua = super::lua();
// The recursion limit error must name the actual function, not a stale one
let err = lua
.load(
r#"
local function buildDeep(depth)
if depth == 0 then
return {val = 1}
end
return {nested = buildDeep(depth - 1)}
end
return table.deepMerge(buildDeep(101), buildDeep(101))
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("table.deepMerge"));
}
#[test]
fn test_ireduce() {
let lua = super::lua();
// Iterates in sequence order - order-sensitive reduction proves it
let result: String = lua
.load(
r#"
local t = {"a", "b", "c", "d"}
return table.ireduce(t, function(acc, v) return acc .. v end, "")
"#,
)
.eval()
.unwrap();
assert_eq!(result, "abcd");
// Empty table returns the initial value
let result: i64 = lua
.load(r#"return table.ireduce({}, function(acc, v) return acc + v end, 42)"#)
.eval()
.unwrap();
assert_eq!(result, 42);
// Only the sequence part is reduced, other keys are ignored
let result: i64 = lua
.load(
r#"
local t = {1, 2, 3}
t.x = 100
return table.ireduce(t, function(acc, v) return acc + v end, 0)
"#,
)
.eval()
.unwrap();
assert_eq!(result, 6);
}
#[test]
fn test_reverse() {
let lua = super::lua();
// Reverse array of numbers
let result: String = lua
.load(r#"return utils.toJSON(table.reverse({1, 2, 3, 4, 5}))"#)
.eval()
.unwrap();
assert_eq!(result, "[5,4,3,2,1]");
// Reverse array of strings
let result: String = lua
.load(r#"return utils.toJSON(table.reverse({"a", "b", "c"}))"#)
.eval()
.unwrap();
assert_eq!(result, r#"["c","b","a"]"#);
// Reverse empty array
let result: String = lua.load(r#"return utils.toJSON(table.reverse({}))"#).eval().unwrap();
assert_eq!(result, "{}");
// Reverse single element
let result: String = lua.load(r#"return utils.toJSON(table.reverse({42}))"#).eval().unwrap();
assert_eq!(result, "[42]");
// Original array unmodified
let result: String = lua
.load(
r#"
local t = {1, 2, 3}
local _ = table.reverse(t)
return utils.toJSON(t)
"#,
)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3]");
// Reverse of reverse is original
let result: String = lua
.load(r#"return utils.toJSON(table.reverse(table.reverse({1, 2, 3, 4})))"#)
.eval()
.unwrap();
assert_eq!(result, "[1,2,3,4]");
}
#[test]
fn test_find() {
let lua = super::lua();
// Find in a sequence returns the value and its key
let result: bool = lua
.load(
r#"
local v, k = table.find({10, 20, 30}, function(x) return x == 20 end)
return v == 20 and k == 2
"#,
)
.eval()
.unwrap();
assert!(result);
// Find in a dictionary
let result: bool = lua
.load(
r#"
local v, k = table.find({a = 1, b = 2}, function(x) return x == 2 end)
return v == 2 and k == "b"
"#,
)
.eval()
.unwrap();
assert!(result);
// Not found returns nil, nil
let result: bool = lua
.load(
r#"
local v, k = table.find({1, 2, 3}, function() return false end)
return v == nil and k == nil
"#,
)
.eval()
.unwrap();
assert!(result);
// Empty table finds nothing
let result: bool = lua
.load(
r#"
local v, k = table.find({}, function() return true end)
return v == nil and k == nil
"#,
)
.eval()
.unwrap();
assert!(result);
// A stored false value is findable - the key distinguishes it from "not found"
let result: bool = lua
.load(
r#"
local v, k = table.find({false}, function(x) return x == false end)
return v == false and k == 1
"#,
)
.eval()
.unwrap();
assert!(result);
}
#[test]
fn test_deep_copy() {
let lua = super::lua();
// The copy is structurally equal to the original
// (key order in the JSON dumps is unspecified, compare parsed)
let (copy, orig): (String, String) = lua
.load(
r#"
local orig = {a = 1, nested = {b = 2, deeper = {c = 3}}, arr = {1, 2, 3}}
local copy = table.deepCopy(orig)
return utils.toJSON(copy), utils.toJSON(orig)
"#,
)
.eval()
.unwrap();
assert!(json_eq(&copy, &orig));
// Mutating the copy at any level must not touch the original
let result: bool = lua
.load(
r#"
local orig = {a = 1, nested = {b = 2}, arr = {1, 2, 3}}
local copy = table.deepCopy(orig)
copy.a = 99
copy.nested.b = 99
copy.arr[1] = 99
return orig.a == 1 and orig.nested.b == 2 and orig.arr[1] == 1
"#,
)
.eval()
.unwrap();
assert!(result);
// Metatables are not copied
let result: bool = lua
.load(
r#"
local orig = setmetatable({x = 1}, {__index = function() return 42 end})
local copy = table.deepCopy(orig)
return getmetatable(copy) == nil and copy.x == 1 and copy.missing == nil
"#,
)
.eval()
.unwrap();
assert!(result);
// Recursion limit protection (also catches self-referencing tables)
let err = lua
.load(
r#"
local function buildDeep(depth)
if depth == 0 then
return {val = 1}
end
return {nested = buildDeep(depth - 1)}
end
return table.deepCopy(buildDeep(101))
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("recursion too deep"));
let err = lua
.load(
r#"
local t = {}
t.self = t
return table.deepCopy(t)
"#,
)
.exec()
.unwrap_err();
assert!(err.to_string().contains("recursion too deep"));
}
#[test]
fn test_argument_type_validation() {
let lua = super::lua();
// Every table.* function must reject wrong argument types with a clear message
let cases: &[(&str, &str)] = &[
("table.filter(5, function() end)", "not a table"),
("table.filter({}, 5)", "not a function"),
("table.ifilter(nil, function() end)", "not a table"),
("table.ifilter({}, nil)", "not a function"),
("table.map(5, function() end)", "not a table"),
("table.map({}, 'x')", "not a function"),
("table.filterMap(5, function() end)", "not a table"),
("table.filterMap({}, 5)", "not a function"),
("table.ifilterMap(5, function() end)", "not a table"),
("table.ifilterMap({}, 5)", "not a function"),
("table.reduce(5, function() end, 0)", "not a table"),
("table.reduce({}, 5, 0)", "not a function"),
("table.ireduce(5, function() end, 0)", "not a table"),
("table.ireduce({}, 5, 0)", "not a function"),
("table.min(5)", "not a table"),
("table.max('foo')", "not a table"),
("table.mean(nil)", "not a table"),
("table.sum(5)", "not a table"),
("table.keys(5)", "not a table"),
("table.values(5)", "not a table"),
("table.contains(5, 1)", "not a table"),
("table.find(5, function() end)", "not a table"),
("table.find({}, 5)", "not a function"),
("table.isEmpty(5)", "not a table"),
("table.reverse(5)", "not a table"),
("table.deepCopy(5)", "not a table"),
("table.deepCopy(nil)", "not a table"),
];
for (code, expected) in cases {
let err = lua.load(format!("return {code}")).exec().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(expected),
"case `{code}` should error with `{expected}`, got: {msg}"
);
}
}
#[test]
fn test_readonly_arg_validation() {
let lua = super::lua();
// Non-table argument must error with the same message style as the other table functions
let err = lua.load(r#"return table.readonly(5)"#).exec().unwrap_err();
assert!(err.to_string().contains("not a table"), "got: {err}");
let err = lua.load(r#"return table.readonly(nil)"#).exec().unwrap_err();
assert!(err.to_string().contains("not a table"), "got: {err}");
// A non-string name must error instead of being silently coerced
let err = lua.load(r#"return table.readonly({}, 5)"#).exec().unwrap_err();
assert!(err.to_string().contains("not a string"), "got: {err}");
}