use omnilua::{Lua, LuaVersion};
const ALL_VERSIONS: [LuaVersion; 5] = [
LuaVersion::V51,
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
const V52_PLUS: [LuaVersion; 4] = [
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
fn eval_str(version: LuaVersion, code: &str) -> String {
let lua = Lua::new_versioned(version);
lua.load(code)
.eval::<String>()
.unwrap_or_else(|e| panic!("eval under {version:?} failed: {e:?}\ncode:\n{code}"))
}
const KNOWN_FN: &str = "local function known(a, b)\n local x = a + b\n return x\nend\nreturn known";
#[test]
fn getinfo_known_function_core_fields_all_versions() {
let probe = format!(
"local known = (loadstring or load)([[{KNOWN_FN}]], '=known')()\n\
local i = debug.getinfo(known, 'Sln')\n\
return tostring(i.what)..'|'..tostring(i.source)..'|'..tostring(i.short_src)\n\
..'|'..tostring(i.linedefined)..'|'..tostring(i.lastlinedefined)\n\
..'|'..tostring(i.name)..'|'..tostring(i.namewhat)"
);
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, &probe),
"Lua|=known|known|1|4|nil|",
"getinfo core fields diverged under {v:?}"
);
}
}
#[test]
fn getinfo_u_option_nparams_isvararg_is_v52_plus() {
let probe = format!(
"local known = (loadstring or load)([[{KNOWN_FN}]], '=known')()\n\
local i = debug.getinfo(known, 'u')\n\
return tostring(i.nups)..'|'..tostring(i.nparams)..'|'..tostring(i.isvararg)"
);
assert_eq!(
eval_str(LuaVersion::V51, &probe),
"0|nil|nil",
"5.1 getinfo 'u' must report only nups (no nparams/isvararg)"
);
for v in V52_PLUS {
assert_eq!(
eval_str(v, &probe),
"0|2|false",
"{v:?} getinfo 'u' must report nups, nparams, isvararg"
);
}
}
#[test]
fn getinfo_currentline_at_known_line_all_versions() {
let probe = "\nreturn tostring(debug.getinfo(1, 'l').currentline)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"2",
"getinfo currentline diverged under {v:?}"
);
}
}
#[test]
fn getinfo_c_function_what_and_short_src_all_versions() {
let probe = "local i = debug.getinfo(print)\n\
return tostring(i.what)..'|'..tostring(i.short_src)";
for v in ALL_VERSIONS {
assert_eq!(eval_str(v, probe), "C|[C]", "getinfo(C fn) diverged under {v:?}");
}
}
#[test]
fn getlocal_by_index_on_running_frame_all_versions() {
let probe = "local function withlocals(p, q)\n\
local aa = 10\n\
local n1, v1 = debug.getlocal(1, 1)\n\
local n2, v2 = debug.getlocal(1, 2)\n\
local n3, v3 = debug.getlocal(1, 3)\n\
return tostring(n1)..'='..tostring(v1)..'|'\n\
..tostring(n2)..'='..tostring(v2)..'|'\n\
..tostring(n3)..'='..tostring(v3)\n\
end\n\
return withlocals(7, 8)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"p=7|q=8|aa=10",
"getlocal by index diverged under {v:?}"
);
}
}
#[test]
fn getlocal_function_param_name_form_is_v52_plus() {
let probe_ok = format!(
"local known = (loadstring or load)([[{KNOWN_FN}]], '=known')()\n\
return tostring(debug.getlocal(known, 1))..'|'\n\
..tostring(debug.getlocal(known, 2))..'|'\n\
..tostring(debug.getlocal(known, 3))"
);
for v in V52_PLUS {
assert_eq!(
eval_str(v, &probe_ok),
"a|b|nil",
"{v:?} getlocal(func, n) must return parameter names"
);
}
let probe_err = format!(
"local known = (loadstring or load)([[{KNOWN_FN}]], '=known')()\n\
local ok = pcall(debug.getlocal, known, 1)\n\
return tostring(ok)"
);
assert_eq!(
eval_str(LuaVersion::V51, &probe_err),
"false",
"5.1 getlocal(func, n) must error (function form is 5.2+)"
);
}
#[test]
fn setlocal_by_index_mutates_and_returns_name_all_versions() {
let probe = "local function setl()\n\
local z = 1\n\
local name = debug.setlocal(1, 1, 42)\n\
return tostring(name)..'|'..tostring(z)\n\
end\n\
return setl()";
for v in ALL_VERSIONS {
assert_eq!(eval_str(v, probe), "z|42", "setlocal diverged under {v:?}");
}
}
#[test]
fn upvalue_get_set_roundtrip_all_versions() {
let probe = "local up = 100\n\
local function usesup() up = up + 1; return up end\n\
local gn, gv = debug.getupvalue(usesup, 1)\n\
local sn = debug.setupvalue(usesup, 1, 555)\n\
return tostring(gn)..'='..tostring(gv)..'|'\n\
..tostring(sn)..'|'..tostring(usesup())";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"up=100|up|556",
"upvalue get/set round-trip diverged under {v:?}"
);
}
}
#[test]
fn traceback_structure_message_then_header_then_frames_all_versions() {
let probe = "local src = 'local function l3() return debug.traceback(\\'MYMSG\\', 1) end\\n'\n\
..'local function l2() return l3() end\\n'\n\
..'return l2()'\n\
local tb = (loadstring or load)(src, '=t')()\n\
local lines = {}\n\
for line in tb:gmatch('[^\\n]+') do lines[#lines + 1] = line end\n\
-- first line is the message; second is the header; pin a frame shape\n\
local has_chunk_frame = false\n\
for _, l in ipairs(lines) do\n\
if l:find('t:1:', 1, true) then has_chunk_frame = true end\n\
end\n\
return lines[1]..'|'..lines[2]..'|'..tostring(has_chunk_frame)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"MYMSG|stack traceback:|true",
"traceback structure diverged under {v:?}"
);
}
}
#[test]
fn traceback_nonstring_message_returned_unchanged_all_versions() {
let probe = "local t = {}\n\
return tostring(debug.traceback(t) == t)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"true",
"traceback(non-string) must return the message unchanged under {v:?}"
);
}
}
#[test]
fn sethook_count_hook_fires_and_gethook_reports_mask_all_versions() {
let probe = "local fires = 0\n\
debug.sethook(function() fires = fires + 1 end, '', 1)\n\
local x = 0\n\
for i = 1, 50 do x = x + i end\n\
debug.sethook()\n\
debug.sethook(function() end, 'l', 0)\n\
local _, mask = debug.gethook()\n\
debug.sethook()\n\
return tostring(fires > 0)..'|'..tostring(mask)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"true|l",
"count-hook firing / gethook mask diverged under {v:?}"
);
}
}
#[test]
fn gethook_with_no_hook_is_nil_all_versions() {
let probe = "return tostring(debug.gethook())";
for v in ALL_VERSIONS {
assert_eq!(eval_str(v, probe), "nil", "gethook (no hook) diverged under {v:?}");
}
}
#[test]
fn getinfo_level_on_suspended_coroutine_all_versions() {
let probe = "local co = coroutine.create(function()\n\
local x = 42\n\
coroutine.yield()\n\
return x\n\
end)\n\
coroutine.resume(co)\n\
local info = debug.getinfo(co, 1, 'nSl')\n\
return tostring(info.currentline)..'|'..tostring(info.what)\n\
..'|'..tostring(info.namewhat)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"3|Lua|",
"cross-thread getinfo(co, level, ...) diverged under {v:?}"
);
}
}
#[test]
fn getinfo_function_argument_cross_thread_on_suspended_coroutine_all_versions() {
let probe = "local function target(a, b)\n\
return a + b\n\
end\n\
local co = coroutine.create(function()\n\
coroutine.yield()\n\
end)\n\
coroutine.resume(co)\n\
local info = debug.getinfo(co, target, 'nS')\n\
return tostring(info.what)..'|'..tostring(info.name)\n\
..'|'..tostring(info.namewhat)";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"Lua|nil|",
"cross-thread getinfo(co, function, ...) diverged under {v:?}"
);
}
}
#[test]
fn getinfo_function_argument_on_active_ancestor_all_versions() {
let probe = "local function target(a, b)\n\
return a + b\n\
end\n\
local parent\n\
local result\n\
local child = coroutine.create(function()\n\
local info = debug.getinfo(parent, target, 'SfL')\n\
result = tostring(info.what)..'|'..tostring(info.func == target)\n\
..'|'..tostring(type(info.activelines))\n\
end)\n\
parent = coroutine.create(function()\n\
coroutine.resume(child)\n\
end)\n\
coroutine.resume(parent)\n\
return result";
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
"Lua|true|table",
"getinfo(active-ancestor, function, ...) diverged under {v:?}"
);
}
}
#[test]
fn getinfo_default_options_are_version_gated() {
let probe = "local i = debug.getinfo(1)\n\
return tostring(i.istailcall ~= nil)..'|'\n\
..tostring(i.nparams ~= nil)..'|'\n\
..tostring(i.ftransfer ~= nil)..'|'\n\
..tostring(i.extraargs ~= nil)";
let expected = |v: LuaVersion| match v {
LuaVersion::V51 => "false|false|false|false",
LuaVersion::V52 | LuaVersion::V53 => "true|true|false|false",
LuaVersion::V54 => "true|true|true|false",
LuaVersion::V55 => "true|true|true|true",
_ => "true|true|true|true",
};
for v in ALL_VERSIONS {
assert_eq!(
eval_str(v, probe),
expected(v),
"getinfo default-option fields diverged under {v:?}"
);
}
}
#[test]
fn getinfo_t_and_r_options_are_version_gated() {
let t_probe = "local ok, err = pcall(debug.getinfo, function() end, 't')\n\
return tostring(ok)..'|'..tostring(ok or err:match('invalid option') ~= nil)";
let r_probe = "local ok, err = pcall(debug.getinfo, function() end, 'r')\n\
return tostring(ok)..'|'..tostring(ok or err:match('invalid option') ~= nil)";
for v in ALL_VERSIONS {
let t_ok = !matches!(v, LuaVersion::V51);
assert_eq!(
eval_str(v, t_probe),
format!("{t_ok}|true"),
"getinfo 't' option gating diverged under {v:?}"
);
let r_ok = matches!(v, LuaVersion::V54 | LuaVersion::V55);
assert_eq!(
eval_str(v, r_probe),
format!("{r_ok}|true"),
"getinfo 'r' option gating diverged under {v:?}"
);
}
}