use luau::{Error, Function, Lua, ThreadStatus};
#[test]
fn thread_resume() -> Result<(), Error> {
let lua = Lua::new()?;
let thread = lua
.load(
r#"
local sum = ...
for _ = 1, 4 do
sum += coroutine.yield(sum)
end
return sum
"#,
)
.into_thread()?;
assert_eq!(thread.status(), ThreadStatus::Resumable);
assert_eq!(thread.resume::<i64>(0)?, 0);
assert_eq!(thread.status(), ThreadStatus::Resumable);
assert_eq!(thread.resume::<i64>(1)?, 1);
assert_eq!(thread.resume::<i64>(2)?, 3);
assert_eq!(thread.resume::<i64>(3)?, 6);
assert_eq!(thread.resume::<i64>(4)?, 10);
assert_eq!(thread.status(), ThreadStatus::Finished);
assert!(matches!(
thread.resume::<()>(()),
Err(Error::CoroutineUnresumable)
));
Ok(())
}
#[test]
fn sandboxed_chunk_thread_binds_an_isolated_environment() -> Result<(), Error> {
let lua = Lua::new()?;
lua.globals()?.set("shared_global", 1)?;
lua.sandbox(true)?;
lua.globals()?.set("shared_global", 2)?;
let chunk = lua
.load("file_global = 42; return file_global, shared_global")
.into_sandboxed()?;
{
let function = chunk.function();
lua.gc_collect()?;
assert_eq!(function.call::<(i32, i32)>(())?, (42, 1));
}
let thread = chunk.into_thread();
assert_eq!(thread.resume::<(i32, i32)>(())?, (42, 1));
assert_eq!(lua.globals()?.get::<Option<i32>>("file_global")?, None);
Ok(())
}
#[test]
fn thread_sandbox_inherits_current_globals_without_stale_imports() -> Result<(), Error> {
let lua = Lua::new()?;
lua.globals()?.set("x", 1)?;
lua.sandbox(true)?;
let load_parent_setter = lua.create_function(luau::callback!(|lua| {
lua.load("return function(value) x = value end")
.eval::<Function<'_>>()
}))?;
let set_parent: Function<'_> = load_parent_setter.call(())?;
lua.current_thread().sandbox()?;
let get_x = lua.load("return x").into_function()?;
assert_eq!(get_x.call::<i32>(())?, 1);
set_parent.call::<()>(2)?;
assert_eq!(get_x.call::<i32>(())?, 2);
Ok(())
}
#[test]
fn thread_resume_error_is_raised_at_the_suspended_yield() -> Result<(), Error> {
let lua = Lua::new()?;
let thread: luau::Thread<'_> = lua
.load(
r#"
return coroutine.create(function()
local ok, error = pcall(coroutine.yield, 123)
assert(not ok)
assert(error == "injected error")
return "recovered"
end)
"#,
)
.eval()?;
assert_eq!(thread.resume::<i32>(())?, 123);
assert_eq!(
thread.resume_error::<String>("injected error")?,
"recovered"
);
Ok(())
}
#[test]
fn thread_resume_inherits_the_active_callback_depth() {
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(|| {
let lua = Lua::new().expect("Lua should initialize");
let resume = lua
.create_function(|lua, mut arguments| {
let depth: i32 = arguments.next()?;
if depth > 0 {
let thread = lua.load("return rust_resume(...)").into_thread()?;
thread.resume::<()>(depth - 1)?;
}
arguments.finish(())
})
.expect("the callback should be created");
lua.globals()
.and_then(|globals| globals.set("rust_resume", resume))
.expect("the callback should be registered");
let thread = lua
.load("return rust_resume(...)")
.into_thread()
.expect("the coroutine should be created");
let error = thread
.resume::<()>(256)
.expect_err("nested native resumes must observe Luau's call-depth limit");
assert!(error.to_string().contains("C stack overflow"), "{error}");
})
.expect("the test thread should start")
.join()
.expect("the test thread should finish");
}
#[test]
fn thread_runtime_errors_include_the_failed_coroutine_traceback() -> Result<(), Error> {
let lua = Lua::new()?;
let thread = lua
.load(
r#"
local function fail()
error("expected failure")
end
fail()
"#,
)
.set_name("thread_traceback")
.into_thread()?;
let error = thread
.resume::<()>(())
.expect_err("the coroutine should fail");
let message = error.to_string();
assert!(message.contains("expected failure"));
assert!(message.contains("stack traceback:"));
assert!(message.contains("thread_traceback"));
Ok(())
}