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.
1497 lines
41 KiB
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(©, &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}");
|
|
}
|
|
|