#![cfg(feature = "tokio")]
use mlua_isle::{AsyncIsle, IsleError};
use std::time::{Duration, Instant};
#[tokio::test]
async fn async_eval_simple() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.eval("return 1 + 2").await.unwrap();
assert_eq!(result, "3");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_eval_string() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.eval("return 'hello world'").await.unwrap();
assert_eq!(result, "hello world");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_eval_nil() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.eval("return nil").await.unwrap();
assert_eq!(result, "");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_eval_lua_error() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.eval("error('boom')").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("boom"));
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_init_sets_globals() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.globals().set("my_val", 42)?;
Ok(())
})
.await
.unwrap();
let result = isle.eval("return my_val").await.unwrap();
assert_eq!(result, "42");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_call_global_function() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
let f = lua.create_function(|_lua, args: mlua::MultiValue| {
let mut parts = Vec::new();
for v in args {
match v {
mlua::Value::String(s) => parts.push(s.to_str().unwrap().to_string()),
_ => parts.push(format!("{v:?}")),
}
}
Ok(parts.join(", "))
})?;
lua.globals().set("greet", f)?;
Ok(())
})
.await
.unwrap();
let result = isle.call("greet", &["hello", "world"]).await.unwrap();
assert_eq!(result, "hello, world");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_exec_closure() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle
.exec(|lua| {
let val: i64 = lua.load("return 7 * 6").eval().map_err(IsleError::from)?;
Ok(val.to_string())
})
.await
.unwrap();
assert_eq!(result, "42");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_spawn_eval_cancel() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task = isle.spawn_eval("while true do end");
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let start = Instant::now();
let result = task.await;
let elapsed = start.elapsed();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), IsleError::Cancelled);
assert!(
elapsed < Duration::from_secs(2),
"cancel took too long: {elapsed:?}"
);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_spawn_call_cancel() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.load("function spin() while true do end end").exec()?;
Ok(())
})
.await
.unwrap();
let task = isle.spawn_call("spin", &[]);
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let result = task.await;
assert_eq!(result.unwrap_err(), IsleError::Cancelled);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_spawn_exec_cancel() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task = isle.spawn_exec(|lua| {
let _: () = lua
.load("while true do end")
.exec()
.map_err(IsleError::from)?;
Ok("done".to_string())
});
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let result = task.await;
assert_eq!(result.unwrap_err(), IsleError::Cancelled);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_multiple_sequential_evals() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.globals().set("counter", 0)?;
Ok(())
})
.await
.unwrap();
for i in 1..=5 {
let result = isle
.eval("counter = counter + 1; return counter")
.await
.unwrap();
assert_eq!(result, i.to_string());
}
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_concurrent_evals_from_multiple_tasks() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task_count = 10;
let mut handles = Vec::with_capacity(task_count);
for i in 0..task_count {
let isle = isle.clone();
handles.push(tokio::spawn(async move {
let code = format!("return {i} * 3");
let result = isle.eval(&code).await.unwrap();
assert_eq!(result, (i * 3).to_string());
}));
}
for h in handles {
h.await.expect("tokio task panicked");
}
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_init_error_propagates() {
let result = AsyncIsle::spawn(|lua| {
lua.load("this is not valid lua").exec()?;
Ok(())
})
.await;
assert!(result.is_err());
match result.err().unwrap() {
IsleError::Init(msg) => {
assert!(!msg.is_empty());
}
other => panic!("expected Init error, got: {other}"),
}
}
#[tokio::test]
async fn async_is_alive_handle() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
assert!(isle.is_alive());
driver.shutdown().await.unwrap();
assert!(!isle.is_alive());
}
#[tokio::test]
async fn async_is_alive_driver() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
assert!(driver.is_alive());
drop(isle);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_drop_without_shutdown() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let _ = isle.eval("return 1").await;
drop(isle);
drop(driver);
}
#[tokio::test]
async fn async_still_works_after_cancel() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task = isle.spawn_eval("while true do end");
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(30)).await;
token.cancel();
});
let _ = task.await;
let result = isle.eval("return 'still alive'").await.unwrap();
assert_eq!(result, "still alive");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_channel_full_returns_correct_error() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let blocker = isle.spawn_eval("while true do end");
let blocker_token = blocker.cancel_token().clone();
tokio::time::sleep(Duration::from_millis(20)).await;
let mut last_task = None;
for _ in 0..300 {
last_task = Some(isle.spawn_eval("return 1"));
}
let result = last_task.unwrap().await;
assert_eq!(result, Err(IsleError::ChannelFull));
blocker_token.cancel();
let _ = blocker.await;
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_clone_independence() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let isle2 = isle.clone();
let r1 = isle.eval("return 1").await.unwrap();
drop(isle);
let r2 = isle2.eval("return 2").await.unwrap();
assert_eq!(r1, "1");
assert_eq!(r2, "2");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn async_driver_drop_does_not_kill_handles() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let isle2 = isle.clone();
drop(driver);
let r1 = isle.eval("return 'from isle'").await.unwrap();
let r2 = isle2.eval("return 'from isle2'").await.unwrap();
assert_eq!(r1, "from isle");
assert_eq!(r2, "from isle2");
drop(isle);
drop(isle2);
}
#[tokio::test]
async fn async_natural_shutdown_via_channel_disconnect() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let _ = isle.eval("return 1").await.unwrap();
drop(isle);
drop(driver);
tokio::time::sleep(Duration::from_millis(50)).await;
}
#[tokio::test]
async fn builder_default_works() {
let (isle, driver) = AsyncIsle::builder().spawn(|_lua| Ok(())).await.unwrap();
let result = isle.eval("return 42").await.unwrap();
assert_eq!(result, "42");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn builder_custom_capacity() {
let (isle, driver) = AsyncIsle::builder()
.channel_capacity(8)
.spawn(|_lua| Ok(()))
.await
.unwrap();
let result = isle.eval("return 'ok'").await.unwrap();
assert_eq!(result, "ok");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn builder_custom_thread_name() {
let (isle, driver) = AsyncIsle::builder()
.thread_name("my-lua-worker")
.spawn(|_lua| Ok(()))
.await
.unwrap();
let result = isle.eval("return 'named'").await.unwrap();
assert_eq!(result, "named");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn builder_small_capacity_triggers_channel_full() {
let (isle, driver) = AsyncIsle::builder()
.channel_capacity(2)
.spawn(|_lua| Ok(()))
.await
.unwrap();
let blocker = isle.spawn_eval("while true do end");
let blocker_token = blocker.cancel_token().clone();
tokio::time::sleep(Duration::from_millis(20)).await;
let mut last_task = None;
for _ in 0..10 {
last_task = Some(isle.spawn_eval("return 1"));
}
let result = last_task.unwrap().await;
assert_eq!(result, Err(IsleError::ChannelFull));
blocker_token.cancel();
let _ = blocker.await;
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn builder_all_options() {
let (isle, driver) = AsyncIsle::builder()
.channel_capacity(32)
.thread_name("custom-isle")
.spawn(|lua| {
lua.globals().set("x", 99)?;
Ok(())
})
.await
.unwrap();
let result = isle.eval("return x").await.unwrap();
assert_eq!(result, "99");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_simple() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.coroutine_eval("return 1 + 2").await.unwrap();
assert_eq!(result, "3");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_string() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.coroutine_eval("return 'hello'").await.unwrap();
assert_eq!(result, "hello");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_nil() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.coroutine_eval("return nil").await.unwrap();
assert_eq!(result, "");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_error() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let result = isle.coroutine_eval("error('boom')").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("boom"));
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_accesses_globals() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.globals().set("val", 42)?;
Ok(())
})
.await
.unwrap();
let result = isle.coroutine_eval("return val * 2").await.unwrap();
assert_eq!(result, "84");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_call_simple() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.load("function add(a, b) return a .. b end").exec()?;
Ok(())
})
.await
.unwrap();
let result = isle
.coroutine_call("add", &["hello", " world"])
.await
.unwrap();
assert_eq!(result, "hello world");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_eval_cancel() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task = isle.spawn_coroutine_eval("while true do end");
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let start = Instant::now();
let result = task.await;
assert_eq!(result.unwrap_err(), IsleError::Cancelled);
assert!(
start.elapsed() < Duration::from_secs(2),
"cancel took too long"
);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_still_works_after_cancel() {
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await.unwrap();
let task = isle.spawn_coroutine_eval("while true do end");
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(30)).await;
token.cancel();
});
let _ = task.await;
let result = isle.coroutine_eval("return 'ok'").await.unwrap();
assert_eq!(result, "ok");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_concurrent_with_async_function() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
let sleep_fn = lua.create_async_function(|_, ms: u64| async move {
tokio::time::sleep(Duration::from_millis(ms)).await;
Ok(ms)
})?;
lua.globals().set("async_sleep", sleep_fn)?;
Ok(())
})
.await
.unwrap();
let start = Instant::now();
let t1 = isle.spawn_coroutine_eval("return async_sleep(50)");
let t2 = isle.spawn_coroutine_eval("return async_sleep(50)");
let (r1, r2) = tokio::join!(t1, t2);
let elapsed = start.elapsed();
assert_eq!(r1.unwrap(), "50");
assert_eq!(r2.unwrap(), "50");
assert!(
elapsed < Duration::from_millis(90),
"coroutines ran sequentially ({elapsed:?}), expected cooperative interleaving"
);
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_mixed_with_sync() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.globals().set("counter", 0)?;
Ok(())
})
.await
.unwrap();
let r1 = isle
.eval("counter = counter + 1; return counter")
.await
.unwrap();
assert_eq!(r1, "1");
let r2 = isle
.coroutine_eval("counter = counter + 10; return counter")
.await
.unwrap();
assert_eq!(r2, "11");
let r3 = isle.eval("return counter").await.unwrap();
assert_eq!(r3, "11");
driver.shutdown().await.unwrap();
}
#[tokio::test]
async fn coroutine_pending_drained_on_shutdown() {
let (isle, driver) = AsyncIsle::spawn(|lua| {
let sleep_fn = lua.create_async_function(|_, ms: u64| async move {
tokio::time::sleep(Duration::from_millis(ms)).await;
Ok(ms)
})?;
lua.globals().set("async_sleep", sleep_fn)?;
Ok(())
})
.await
.unwrap();
let task = isle.spawn_coroutine_eval("return async_sleep(80)");
tokio::time::sleep(Duration::from_millis(10)).await;
driver.shutdown().await.unwrap();
let result = task.await;
assert_eq!(result.unwrap(), "80");
}