//! 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}"); }