use std::cell::Cell;
use std::io;
use std::rc::Rc;
use luau::{Error, InterruptAction, InterruptHooks, InterruptMode, Lua, Result};
#[test]
fn memory_limit_rejects_growth_and_can_be_disabled() -> Result<()> {
let lua = Lua::new()?;
let used = lua.used_memory();
assert!(used > 0);
assert_eq!(lua.set_memory_limit(used + 1024), 0);
assert!(matches!(
lua.create_buffer_with_capacity(1024 * 1024),
Err(Error::MemoryError(_))
));
assert_eq!(lua.set_memory_limit(0), used + 1024);
assert_eq!(
lua.create_buffer_with_capacity(1024 * 1024)?.len(),
1024 * 1024
);
Ok(())
}
#[test]
fn application_data_is_available_to_callbacks_with_checked_borrows() -> Result<()> {
let lua = Lua::new()?;
lua.set_app_data(40_i32);
lua.globals()?.set(
"increment",
luau::function!(|lua| {
let mut value = lua
.app_data_mut::<i32>()
.expect("application data should be installed");
*value += 1;
Ok(*value)
}),
)?;
assert_eq!(lua.load("return increment()").call::<i32>(())?, 41);
{
let value = lua
.app_data_ref::<i32>()
.expect("application data should remain installed");
assert!(lua.try_set_app_data(0_i32).is_err());
assert_eq!(*value, 41);
}
assert_eq!(lua.remove_app_data::<i32>(), Some(41));
Ok(())
}
#[test]
fn runtime_debug_access_observes_active_luau_frames() -> Result<()> {
let lua = Lua::new()?;
lua.globals()?.set(
"inspect",
luau::function!(|lua| {
let has_line = lua
.inspect_stack(1, |frame| frame.current_line().is_some())?
.expect("callback should have an active Luau caller");
let traceback = lua.traceback(Some("trace marker"), 0)?;
Ok((has_line, traceback.to_str()?.to_owned()))
}),
)?;
let (has_line, traceback): (bool, String) = lua
.load(
r#"
local function caller()
return inspect()
end
return caller()
"#,
)
.set_name("runtime_debug_access")
.call(())?;
assert!(has_line);
assert!(traceback.contains("trace marker"));
assert!(traceback.contains("runtime_debug_access"));
assert!(lua.gc_is_running()?);
lua.gc_stop()?;
assert!(!lua.gc_is_running()?);
lua.gc_restart()?;
assert!(lua.gc_is_running()?);
let _ = lua.gc_step()?;
Ok(())
}
#[test]
fn interrupt_callbacks_are_non_reentrant_and_resume_after_return() -> Result<()> {
let mut lua = Lua::new()?;
let inside_execution = Rc::new(Cell::new(false));
let execution_calls = Rc::new(Cell::new(0));
let nested_execution_calls = Rc::new(Cell::new(0));
let nested_pattern_calls = Rc::new(Cell::new(0));
let outer_pattern_calls = Rc::new(Cell::new(0));
let execution_inside = Rc::clone(&inside_execution);
let execution_count = Rc::clone(&execution_calls);
let nested_execution_count = Rc::clone(&nested_execution_calls);
let pattern_inside = Rc::clone(&inside_execution);
let nested_pattern_count = Rc::clone(&nested_pattern_calls);
let outer_pattern_count = Rc::clone(&outer_pattern_calls);
lua.set_interrupt_handler(
InterruptHooks::new()
.on_execution(move |context| {
if execution_inside.replace(true) {
nested_execution_count.set(nested_execution_count.get() + 1);
return Ok(InterruptAction::Continue);
}
execution_count.set(execution_count.get() + 1);
let matched = context
.lua()
.load("return string.match('nested', 'n')")
.call::<String>(())?;
execution_inside.set(false);
assert_eq!(matched, "n");
Ok(InterruptAction::Continue)
})
.on_pattern(move |_| {
if pattern_inside.get() {
nested_pattern_count.set(nested_pattern_count.get() + 1);
} else {
outer_pattern_count.set(outer_pattern_count.get() + 1);
}
Ok(())
}),
);
let matched = lua
.load("local n = 0; for i = 1, 10 do n += i end; return string.match('outer', 'o')")
.call::<String>(())?;
lua.remove_interrupt_handler();
assert_eq!(matched, "o");
assert!(execution_calls.get() > 1);
assert_eq!(nested_execution_calls.get(), 0);
assert_eq!(nested_pattern_calls.get(), 0);
assert!(outer_pattern_calls.get() > 0);
Ok(())
}
#[test]
fn requested_interrupt_is_consumed_once_and_can_be_deferred() -> Result<()> {
let mut lua = Lua::new()?;
let interrupt = lua.interrupt_handle();
let calls = Rc::new(Cell::new(0));
let callback_calls = Rc::clone(&calls);
lua.set_interrupt_handler(
InterruptHooks::new()
.set_mode(InterruptMode::Requested)
.on_execution(move |context| {
let current_calls = callback_calls.get();
callback_calls.set(current_calls + 1);
if current_calls == 0 {
context.defer_interrupt();
}
Ok(InterruptAction::Continue)
}),
);
let function = lua
.load("local n = 0; for i = 1, 100 do n += i end; return n")
.into_function()?;
interrupt.request();
lua.gc_step()?;
assert_eq!(function.call::<i64>(())?, 5050);
assert_eq!(calls.get(), 2);
assert_eq!(function.call::<i64>(())?, 5050);
assert_eq!(calls.get(), 2);
Ok(())
}
#[test]
fn callback_errors_expose_their_external_error_chain() -> Result<()> {
let lua = Lua::new()?;
let callback = lua.create_function(|_, _| {
Err(Error::external(io::Error::new(
io::ErrorKind::InvalidData,
"invalid callback data",
)))
})?;
let error = callback
.call::<()>(())
.expect_err("the callback should return its external error");
assert_eq!(
error
.downcast_ref::<io::Error>()
.expect("the external error should be discoverable")
.kind(),
io::ErrorKind::InvalidData
);
let mut chain = error.chain();
assert!(matches!(
chain.next().and_then(|error| error.downcast_ref::<Error>()),
Some(Error::CallbackError { .. })
));
assert_eq!(
chain
.next()
.and_then(|error| error.downcast_ref::<io::Error>())
.expect("the chain should contain the external error")
.kind(),
io::ErrorKind::InvalidData
);
assert!(chain.next().is_none());
Ok(())
}