use omnilua::{Lua, LuaVersion, Value};
const ALL: [LuaVersion; 5] = [
LuaVersion::V51,
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
fn eval_str(version: LuaVersion, code: &str) -> Vec<u8> {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::String(s)) => s
.as_bytes()
.unwrap_or_else(|e| panic!("string bytes under {version:?} for `{code}`: {e:?}"))
.to_vec(),
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected a string"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_int(version: LuaVersion, code: &str) -> i64 {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::Integer(i)) => i,
Ok(Value::Number(n)) if n.fract() == 0.0 => n as i64,
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected an integer"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn assert_true(version: LuaVersion, code: &str) {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::Boolean(true)) => {}
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected true"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_err(version: LuaVersion, code: &str) -> String {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(v) => panic!("expected error under {version:?} for `{code}`, got {v:?}"),
Err(e) => e.message_lossy(),
}
}
#[test]
fn ipairs_consults_index_only_from_5_3_crossversion() {
let probe = "\
local t = setmetatable({}, {__index = function(_, k) \
if k <= 3 then return k * 10 end \
end}) \
local c = 0 \
for i, v in ipairs(t) do c = c + 1 end \
return c";
for v in [LuaVersion::V51, LuaVersion::V52] {
assert_eq!(eval_int(v, probe), 0, "{v:?}: ipairs must be raw (no __index)");
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_int(v, probe), 3, "{v:?}: ipairs must honor __index");
}
}
#[test]
fn ipairs_type_checks_table_only_pre_5_3_crossversion() {
let probe = "for i, v in ipairs('hi') do end return 'ran'";
for v in [LuaVersion::V51, LuaVersion::V52] {
let msg = eval_err(v, probe);
assert!(
msg.contains("table expected") && msg.contains("ipairs"),
"{v:?}: expected a 'table expected' arg error for ipairs, got `{msg}`"
);
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, probe), b"ran", "{v:?}: ipairs over a string must not raise");
}
}
#[test]
fn ipairs_consults_ipairs_metamethod_on_5_2_and_5_3_crossversion() {
let probe = "\
local t = setmetatable({'A', 'B'}, {__ipairs = function(x) \
return function() return nil end, x, 0 \
end}) \
local s = '' \
for i, v in ipairs(t) do s = s .. v end \
return s";
for v in [LuaVersion::V52, LuaVersion::V53] {
assert_eq!(eval_str(v, probe), b"", "{v:?}: __ipairs must suppress iteration");
}
for v in [LuaVersion::V51, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, probe), b"AB", "{v:?}: __ipairs must be ignored");
}
}
#[test]
fn assert_message_must_be_string_pre_5_3_crossversion() {
let probe = "return select(2, pcall(function() assert(false, {code = 7}) end))";
for v in [LuaVersion::V51, LuaVersion::V52] {
let msg = eval_err(
v,
"assert(false, {code = 7})",
);
assert!(
msg.contains("string expected") && msg.contains("assert"),
"{v:?}: assert(false, <table>) must raise a string-expected arg error, got `{msg}`"
);
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
let lua = Lua::new_versioned(v);
let got = lua
.load(probe)
.eval::<Value>()
.unwrap_or_else(|e| panic!("{v:?}: {e:?}"));
assert!(
matches!(got, Value::Table(_)),
"{v:?}: assert(false, <table>) must forward the table object, got {got:?}"
);
}
}
#[test]
fn assert_number_message_is_stringified_pre_5_3_else_forwarded_crossversion() {
let probe = "return select(2, pcall(function() assert(false, 404) end))";
for v in [LuaVersion::V51, LuaVersion::V52] {
let lua = Lua::new_versioned(v);
match lua.load(probe).eval::<Value>() {
Ok(Value::String(s)) => {
let bytes = s.as_bytes().unwrap();
assert!(bytes.ends_with(b"404"), "{v:?}: `{:?}`", bytes);
}
other => panic!("{v:?}: assert(false, 404) must be a prefixed string, got {other:?}"),
}
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
let lua = Lua::new_versioned(v);
match lua.load(probe).eval::<Value>() {
Ok(Value::Integer(404)) | Ok(Value::Number(_)) => {}
other => panic!("{v:?}: assert(false, 404) must forward the number, got {other:?}"),
}
}
}
#[test]
fn assert_string_message_and_default_all_versions() {
for v in ALL {
assert!(eval_err(v, "assert(false, 'boom')").ends_with("boom"), "{v:?}");
assert!(
eval_err(v, "assert(false)").ends_with("assertion failed!"),
"{v:?}"
);
}
}
#[test]
fn rawlen_arg_error_names_function_and_gates_got_suffix_crossversion() {
for v in [LuaVersion::V52, LuaVersion::V53] {
let msg = eval_err(v, "return rawlen(5)");
assert!(
msg.ends_with("bad argument #1 to 'rawlen' (table or string expected)"),
"{v:?}: `{msg}`"
);
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, "return rawlen(5)")
.ends_with("bad argument #1 to 'rawlen' (table or string expected, got number)"),
"{v:?}"
);
assert!(
eval_err(v, "return rawlen(true)")
.ends_with("bad argument #1 to 'rawlen' (table or string expected, got boolean)"),
"{v:?}"
);
assert!(
eval_err(v, "return rawlen(nil)")
.ends_with("bad argument #1 to 'rawlen' (table or string expected, got nil)"),
"{v:?}"
);
}
}
#[test]
fn rawlen_accepts_tables_and_strings_5_2_plus() {
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_int(v, "return rawlen({1, 2, 3})"), 3, "{v:?}");
assert_eq!(eval_int(v, "return rawlen('hello')"), 5, "{v:?}");
assert_eq!(
eval_int(
v,
"return rawlen(setmetatable({1, 2}, {__len = function() return 99 end}))"
),
2,
"{v:?}: rawlen must ignore __len"
);
}
}
#[test]
fn rawlen_absent_on_5_1() {
assert_eq!(eval_str(LuaVersion::V51, "return type(rawlen)"), b"nil");
}
#[test]
fn rawget_rawset_rawequal_bypass_metamethods_all_versions() {
for v in ALL {
assert_eq!(
eval_str(
v,
"local t = setmetatable({}, {__index = function() return 'META' end}) \
t.x = 1 \
return tostring(rawget(t, 'x')) .. ',' .. tostring(rawget(t, 'y'))"
),
b"1,nil",
"{v:?}"
);
assert_eq!(
eval_str(
v,
"local hit = false \
local t = setmetatable({}, {__newindex = function() hit = true end}) \
rawset(t, 'k', 'v') \
return tostring(t.k) .. ',' .. tostring(hit)"
),
b"v,false",
"{v:?}"
);
assert_eq!(
eval_str(
v,
"local m = {__eq = function() return true end} \
local a = setmetatable({}, m) \
local b = setmetatable({}, m) \
return tostring(rawequal(a, b)) .. ',' .. tostring(a == b)"
),
b"false,true",
"{v:?}"
);
}
}
#[test]
fn select_count_and_negative_index_all_versions() {
for v in ALL {
assert_eq!(eval_int(v, "return select('#', 'a', 'b', 'c')"), 3, "{v:?}");
assert_eq!(eval_str(v, "return select(2, 'a', 'b', 'c')"), b"b", "{v:?}");
assert_eq!(eval_str(v, "return select(-1, 'a', 'b', 'c')"), b"c", "{v:?}");
assert_eq!(
eval_str(
v,
"return table.concat({select(-2, 'a', 'b', 'c')}, ',')"
),
b"b,c",
"{v:?}"
);
}
}
#[test]
fn select_out_of_range_index_raises_all_versions() {
for v in ALL {
assert!(
eval_err(v, "return select(0, 'a', 'b')").contains("index out of range"),
"{v:?}"
);
assert!(
eval_err(v, "return select(-9, 'a', 'b')").contains("index out of range"),
"{v:?}"
);
}
}
#[test]
fn tonumber_base_conversion_all_versions() {
for v in ALL {
assert_eq!(eval_int(v, "return tonumber('0x10')"), 16, "{v:?}");
assert_eq!(eval_int(v, "return tonumber('11', 2)"), 3, "{v:?}");
assert_eq!(eval_int(v, "return tonumber('ff', 16)"), 255, "{v:?}");
assert_eq!(eval_int(v, "return tonumber('z', 36)"), 35, "{v:?}");
assert_eq!(eval_str(v, "return tostring(tonumber('2', 2))"), b"nil", "{v:?}");
}
for v in ALL {
assert!(
eval_err(v, "return tonumber('x', 1)").contains("base out of range"),
"{v:?}"
);
assert!(
eval_err(v, "return tonumber('x', 37)").contains("base out of range"),
"{v:?}"
);
}
}
#[test]
fn tonumber_subtype_is_integer_from_5_3() {
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return math.type(tonumber('10'))"), b"integer", "{v:?}");
assert_eq!(eval_str(v, "return math.type(tonumber('10.0'))"), b"float", "{v:?}");
assert_eq!(eval_str(v, "return math.type(tonumber('ff', 16))"), b"integer", "{v:?}");
}
}
#[test]
fn pairs_consults_pairs_metamethod_from_5_2_crossversion() {
let probe = "\
local t = setmetatable({1, 2, 3}, {__pairs = function() \
return function() return nil end, t, nil \
end}) \
local c = 0 \
for k, v in pairs(t) do c = c + 1 end \
return c";
assert_eq!(eval_int(LuaVersion::V51, probe), 3, "5.1 ignores __pairs");
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_int(v, probe), 0, "{v:?} honors __pairs");
}
}
#[test]
fn v51_only_globals_present_only_on_5_1() {
for fname in ["gcinfo", "newproxy", "getfenv", "setfenv"] {
assert_eq!(
eval_str(LuaVersion::V51, &format!("return type({fname})")),
b"function",
"5.1 must expose {fname}"
);
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_str(v, &format!("return type({fname})")),
b"nil",
"{v:?} must NOT expose {fname}"
);
}
}
for fname in ["loadstring", "unpack"] {
for v in [LuaVersion::V51, LuaVersion::V52] {
assert_eq!(
eval_str(v, &format!("return type({fname})")),
b"function",
"{v:?} must expose {fname}"
);
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eq!(
eval_str(v, &format!("return type({fname})")),
b"nil",
"{v:?} must NOT expose {fname}"
);
}
}
}
#[test]
fn warn_present_only_from_5_4() {
for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
assert_eq!(eval_str(v, "return type(warn)"), b"nil", "{v:?}");
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eq!(eval_str(v, "return type(warn)"), b"function", "{v:?}");
}
}
#[test]
fn version_global_string_per_version() {
assert_eq!(eval_str(LuaVersion::V51, "return _VERSION"), b"Lua 5.1");
assert_eq!(eval_str(LuaVersion::V52, "return _VERSION"), b"Lua 5.2");
assert_eq!(eval_str(LuaVersion::V53, "return _VERSION"), b"Lua 5.3");
assert_eq!(eval_str(LuaVersion::V54, "return _VERSION"), b"Lua 5.4");
assert_eq!(eval_str(LuaVersion::V55, "return _VERSION"), b"Lua 5.5");
}
#[test]
fn error_object_and_level_all_versions() {
for v in ALL {
assert_true(
v,
"local ok, e = pcall(function() error({code = 5}) end) \
return (not ok) and type(e) == 'table' and e.code == 5",
);
assert_eq!(
eval_str(
v,
"return select(2, pcall(function() error('boom', 0) end))"
),
b"boom",
"{v:?}"
);
assert_true(
v,
"local function f() error('x', 2) end \
local ok, e = pcall(function() f() end) \
return (not ok) and e:match(': x$') ~= nil",
);
}
}
#[test]
fn getmetatable_honors_protected_metatable_all_versions() {
for v in ALL {
assert_eq!(
eval_str(
v,
"return getmetatable(setmetatable({}, {__metatable = 'LOCKED'}))"
),
b"LOCKED",
"{v:?}"
);
}
}
#[test]
fn tostring_honors_tostring_metamethod_all_versions() {
for v in ALL {
assert_eq!(
eval_str(
v,
"return tostring(setmetatable({}, {__tostring = function() return 'CUSTOM' end}))"
),
b"CUSTOM",
"{v:?}"
);
}
}