use omnilua::{Lua, LuaVersion};
const ALL: [LuaVersion; 5] = [
LuaVersion::V51,
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 of `{code}` failed under {version:?}: {e:?}"))
}
fn assert_eval(version: LuaVersion, code: &str, expected: &str) {
let got = eval_str(version, code);
assert_eq!(
got, expected,
"under {version:?}, `{code}` returned {got:?}, expected {expected:?}"
);
}
#[test]
fn running_in_main_arity_by_version() {
let code = "local a,b = coroutine.running(); return type(a) .. ' ' .. tostring(b)";
assert_eval(LuaVersion::V51, code, "nil nil");
for v in [
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
] {
assert_eval(v, code, "thread true");
}
}
#[test]
fn running_in_coroutine_arity_by_version() {
let code = "local out; \
coroutine.wrap(function() \
local a,b = coroutine.running(); \
out = type(a) .. ' ' .. tostring(b) \
end)(); \
return out";
assert_eval(LuaVersion::V51, code, "thread nil");
for v in [
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
] {
assert_eval(v, code, "thread false");
}
}
#[test]
fn isyieldable_presence_by_version() {
let code = "return type(coroutine.isyieldable)";
assert_eval(LuaVersion::V51, code, "nil");
assert_eval(LuaVersion::V52, code, "nil");
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, code, "function");
}
}
#[test]
fn isyieldable_main_vs_coroutine() {
let main_code = "return tostring(coroutine.isyieldable())";
let coro_code = "local out; \
coroutine.wrap(function() \
out = tostring(coroutine.isyieldable()) \
end)(); \
return out";
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, main_code, "false");
assert_eval(v, coro_code, "true");
}
}
#[test]
fn close_presence_by_version() {
let code = "return type(coroutine.close)";
for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
assert_eval(v, code, "nil");
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, code, "function");
}
}
#[test]
fn close_dead_returns_true() {
let code = "local co = coroutine.create(function() end); \
coroutine.resume(co); \
return tostring(coroutine.close(co))";
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, code, "true");
}
}
#[test]
fn close_suspended_transitions_to_dead() {
let code = "local co = coroutine.create(function() coroutine.yield() end); \
coroutine.resume(co); \
local before = coroutine.status(co); \
local ok = coroutine.close(co); \
local after = coroutine.status(co); \
return before .. ' ' .. tostring(ok) .. ' ' .. after";
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, code, "suspended true dead");
}
}
#[test]
fn close_normal_coroutine_errors() {
let code = "local main = coroutine.running(); \
local co = coroutine.create(function() \
local ok, msg = pcall(coroutine.close, main); \
return tostring(ok) .. '|' .. (string.find(msg, 'normal coroutine') ~= nil and 'normal' or msg) \
end); \
local _, r = coroutine.resume(co); \
return r";
for v in [LuaVersion::V54, LuaVersion::V55] {
assert_eval(v, code, "false|normal");
}
}
#[test]
fn close_running_self_5_4_errors_5_5_unwinds() {
let code_54 = "local co; co = coroutine.create(function() \
local ok, msg = pcall(coroutine.close, co); \
return tostring(ok) .. '|' .. (string.find(msg, 'running coroutine') ~= nil and 'running' or msg) \
end); \
local _, r = coroutine.resume(co); \
return tostring(r)";
assert_eval(LuaVersion::V54, code_54, "false|running");
let code_55 = "local marker = 'pre'; \
local co; co = coroutine.create(function() \
marker = 'entered'; \
coroutine.close(co); \
marker = 'after-close' \
end); \
coroutine.resume(co); \
return marker .. ' ' .. coroutine.status(co)";
assert_eval(LuaVersion::V55, code_55, "entered dead");
}
#[test]
fn resume_dead_message_all_versions() {
let code = "local co = coroutine.create(function() end); \
coroutine.resume(co); \
local ok, msg = coroutine.resume(co); \
return tostring(ok) .. '|' .. msg";
for v in ALL {
assert_eval(v, code, "false|cannot resume dead coroutine");
}
}
#[test]
fn double_resume_running_message_by_version() {
let code = "local co; co = coroutine.create(function() \
local ok, msg = coroutine.resume(co); \
return tostring(ok) .. '|' .. msg \
end); \
local _, inner = coroutine.resume(co); \
return inner";
assert_eval(LuaVersion::V51, code, "false|cannot resume running coroutine");
for v in [
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
] {
assert_eval(v, code, "false|cannot resume non-suspended coroutine");
}
}
#[test]
fn wrap_reraises_error_in_caller() {
let code = "local f = coroutine.wrap(function() error('boom', 0) end); \
local ok, msg = pcall(f); \
return tostring(ok) .. '|' .. tostring(msg)";
for v in ALL {
assert_eval(v, code, "false|boom");
}
}
#[test]
fn wrap_forwards_yield_then_return() {
let code = "local f = coroutine.wrap(function() \
coroutine.yield('y1'); \
return 'r1' \
end); \
local a = f(); \
local b = f(); \
return a .. ' ' .. b";
for v in ALL {
assert_eval(v, code, "y1 r1");
}
}
#[test]
fn status_transitions_across_yield() {
let code = "local self_status; \
local co = coroutine.create(function() \
self_status = coroutine.status(coroutine.running()); \
coroutine.yield() \
end); \
local s0 = coroutine.status(co); \
coroutine.resume(co); \
local s1 = coroutine.status(co); \
coroutine.resume(co); \
local s2 = coroutine.status(co); \
return s0 .. ' ' .. self_status .. ' ' .. s1 .. ' ' .. s2";
for v in ALL {
assert_eval(v, code, "suspended running suspended dead");
}
}
#[test]
fn parent_status_is_normal_while_child_runs() {
let code = "local parent_status; \
local parent = coroutine.create(function() \
local child = coroutine.create(function(p) \
parent_status = coroutine.status(p) \
end); \
coroutine.resume(child, coroutine.running()) \
end); \
coroutine.resume(parent); \
return parent_status";
for v in ALL {
assert_eval(v, code, "normal");
}
}