luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
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(())
}