use std::collections::{BTreeMap, HashSet};
use std::ffi::CString;
use bstr::BString;
use luau::{Error, Lua, MultiValue, Value};
#[test]
fn callback_integer_conversion_preserves_argument_error_context() -> Result<(), Error> {
let lua = Lua::new()?;
let function = lua.create_function(luau::callback!(|_lua, value: i32| { Ok(value) }))?;
assert_eq!(function.call::<i32>("42")?, 42);
let error = function
.call::<i32>(i64::MAX)
.expect_err("i64::MAX must not fit into i32");
let Error::CallbackError { cause, .. } = error else {
panic!("expected callback error");
};
let Error::BadArgument { pos, cause, .. } = cause.as_ref() else {
panic!("expected bad argument error");
};
assert_eq!(*pos, 1);
assert!(matches!(
cause.as_ref(),
Error::FromLuaConversionError { from, to, message }
if from == "integer" && to == "i32" && message.as_deref() == Some("out of range")
));
Ok(())
}
#[test]
fn integer_extraction_rejects_unrepresentable_values() -> Result<(), Error> {
let lua = Lua::new()?;
assert_eq!(lua.load("1.5").eval::<i64>()?, 1);
assert!(lua.load("-1").eval::<u64>().is_err());
assert!(lua.load("math.huge").eval::<i64>().is_err());
assert!(lua.unpack::<u64>(lua.pack(1_u128 << 64)?).is_err());
assert_eq!(lua.unpack::<i128>(lua.pack(1_i128 << 64)?)?, 1_i128 << 64);
Ok(())
}
#[test]
fn rust_integers_use_luau_representations_without_precision_loss() -> Result<(), Error> {
let lua = Lua::new()?;
assert!(matches!(lua.pack(42_i32)?, Value::Number(42.0)));
assert!(matches!(lua.pack(42_i64)?, Value::Integer(42)));
assert!(matches!(lua.pack(42_isize)?, Value::Number(42.0)));
assert!(matches!(lua.pack(42_usize)?, Value::Number(42.0)));
assert!(matches!(
lua.pack(i64::MAX as u64)?,
Value::Integer(i64::MAX)
));
let exact_number = 1_u64 << 63;
assert!(matches!(
lua.pack(exact_number)?,
Value::Number(value) if value == exact_number as f64
));
assert_eq!(lua.pack(exact_number)?.as_u64(), Some(exact_number));
assert!(matches!(
lua.pack(exact_number + 1),
Err(Error::IntoLuaConversionError { from, to, message })
if from == "u64" && to == "number" && message.as_deref() == Some("out of range")
));
assert_eq!(lua.pack(42_usize)?.as_usize(), Some(42));
assert_eq!(Value::Number(-1.0).as_u32(), None);
assert_eq!(Value::Number(f64::INFINITY).as_i32(), None);
#[cfg(target_pointer_width = "64")]
{
let exact_pointer_number = 1_usize << 53;
assert!(lua.pack(exact_pointer_number).is_ok());
assert!(matches!(
lua.pack(exact_pointer_number + 1),
Err(Error::IntoLuaConversionError { from, to, message })
if from == "usize"
&& to == "number"
&& message.as_deref() == Some("out of range")
));
}
Ok(())
}
#[test]
fn string_conversion_matches_between_values_and_stack_arguments() -> Result<(), Error> {
let lua = Lua::new()?;
assert_eq!(lua.unpack::<String>(Value::Number(-123.55))?, "-123.55");
let string_argument =
lua.create_function(luau::callback!(|_lua, value: String| { Ok(value) }))?;
assert_eq!(string_argument.call::<String>(-43.22)?, "-43.22");
assert!(lua.unpack::<String>(Value::Integer(123)).is_err());
assert!(string_argument.call::<String>(123_i64).is_err());
let bytes = b"\xffbuffer bytes";
let buffer = lua.create_buffer(bytes)?;
assert_eq!(
lua.unpack::<BString>(Value::Buffer(buffer.try_clone()?))?,
bytes.as_slice()
);
let buffer_argument =
lua.create_function(luau::callback!(|_lua, value: BString| { Ok(value.len()) }))?;
assert_eq!(buffer_argument.call::<usize>(&buffer)?, bytes.len());
Ok(())
}
#[test]
fn rust_collections_use_luau_table_sequence_and_key_semantics() -> Result<(), Error> {
let lua = Lua::new()?;
let sequence = lua.create_sequence_from([10, 20, 30])?;
assert_eq!(sequence.raw_get::<i32>(1)?, 10);
assert_eq!(
lua.unpack::<Vec<i32>>(Value::Table(sequence.try_clone()?))?,
[10, 20, 30]
);
assert_eq!(
lua.unpack::<[i32; 3]>(Value::Table(sequence.try_clone()?))?,
[10, 20, 30]
);
assert!(
lua.unpack::<[i32; 2]>(Value::Table(sequence.try_clone()?))
.is_err()
);
let map = BTreeMap::from([("left".to_owned(), 1), ("right".to_owned(), 2)]);
let packed_map = lua.pack(map.clone())?;
assert_eq!(lua.unpack::<BTreeMap<String, i32>>(packed_map)?, map);
let set = HashSet::from(["alpha".to_owned(), "beta".to_owned()]);
let packed_set = lua.pack(set.clone())?;
assert_eq!(lua.unpack::<HashSet<String>>(packed_set)?, set);
Ok(())
}
#[test]
fn character_and_c_string_conversions_enforce_their_value_domains() -> Result<(), Error> {
let lua = Lua::new()?;
assert_eq!(lua.convert::<char>("A")?, 'A');
assert_eq!(lua.convert::<char>(65_i64)?, 'A');
assert_eq!(lua.convert::<char>(128_175_i64)?, '💯');
assert!(lua.convert::<char>(1.0).is_err());
assert!(lua.convert::<char>("more than one").is_err());
assert!(lua.convert::<char>(5_456_324_i64).is_err());
let c_string = CString::new("embedded value").expect("test string should not contain a nul");
assert_eq!(
lua.unpack::<CString>(lua.pack(c_string.clone())?)?,
c_string
);
let interior_nul = lua.create_string(b"not\0a C string")?;
assert!(lua.unpack::<CString>(Value::String(interior_nul)).is_err());
Ok(())
}
#[test]
fn borrowed_table_and_multi_value_conversions_preserve_identity_and_roots() -> Result<(), Error> {
let lua = Lua::new()?;
let (table_pointer, function_pointer, packed_table, packed_values) = {
let table = lua.create_table()?;
let function: luau::Function<'_> = lua.load("return function() return 42 end").call(())?;
let values = MultiValue::from_vec(vec![
Value::Table(table.try_clone()?),
Value::Function(function.try_clone()?),
]);
(
table.to_pointer(),
function.to_pointer(),
lua.pack(&table)?,
lua.pack_multi(&values)?,
)
};
lua.gc_collect()?;
let Value::Table(packed_table) = packed_table else {
panic!("the borrowed table should remain a table");
};
assert_eq!(packed_table.to_pointer(), table_pointer);
let identity: luau::Function<'_> = lua.load("return function(...) return ... end").call(())?;
let returned_table: luau::Table<'_> = identity.call(&packed_table)?;
let returned_values: MultiValue<'_> = identity.call(&packed_values)?;
assert_eq!(returned_table.to_pointer(), table_pointer);
assert_eq!(returned_values.len(), 2);
let mut returned_values = returned_values.into_iter();
let Some(Value::Table(returned_table)) = returned_values.next() else {
panic!("the first borrowed multi-value should remain a table");
};
let Some(Value::Function(returned_function)) = returned_values.next() else {
panic!("the second borrowed multi-value should remain a function");
};
assert_eq!(returned_table.to_pointer(), table_pointer);
assert_eq!(returned_function.to_pointer(), function_pointer);
Ok(())
}