parent
8160815e3f
commit
e3f580c161
@ -0,0 +1,8 @@ |
|||||||
|
column_width = 120 |
||||||
|
line_endings = "Unix" |
||||||
|
indent_type = "Spaces" |
||||||
|
indent_width = 4 |
||||||
|
quote_style = "AutoPreferDouble" |
||||||
|
call_parentheses = "Always" |
||||||
|
# never allow `if x then y end` on one line — always expand after then/do |
||||||
|
collapse_simple_statement = "Never" |
||||||
@ -0,0 +1,313 @@ |
|||||||
|
//! Tests for the async parts of the stdlib: os.sleep, task.join and sqlite.
|
||||||
|
//! These have no flowbox-rt counterpart and need a tokio runtime, so they live
|
||||||
|
//! apart from the ported sync tests.
|
||||||
|
|
||||||
|
use std::time::{Duration, Instant}; |
||||||
|
|
||||||
|
use super::lua; |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_os_sleep_actually_sleeps() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let start = Instant::now(); |
||||||
|
lua.load(r#"os.sleep(0.1)"#).exec_async().await.unwrap(); |
||||||
|
let elapsed = start.elapsed(); |
||||||
|
|
||||||
|
assert!(elapsed >= Duration::from_millis(95), "slept only {elapsed:?}"); |
||||||
|
assert!(elapsed < Duration::from_millis(500), "slept too long: {elapsed:?}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_os_sleep_invalid_duration() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
// Negative, NaN and infinite durations must raise a Lua error, not panic the host
|
||||||
|
for src in ["os.sleep(-1)", "os.sleep(0/0)", "os.sleep(math.huge)"] { |
||||||
|
let err = lua.load(src).exec_async().await.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("os.sleep"), "{src}: {err}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_os_sleep_rejects_huge_finite_duration() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
// A huge-but-finite duration must be rejected promptly, not parked forever
|
||||||
|
// (which would read as the runtime hanging). The test itself would hang if
|
||||||
|
// this regressed, so tokio's test timeout / the assertion guards it.
|
||||||
|
let err = lua.load(r#"os.sleep(1e18)"#).exec_async().await.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("os.sleep"), "{err}"); |
||||||
|
|
||||||
|
// A duration within the cap is accepted (0 sleeps instantly).
|
||||||
|
lua.load(r#"os.sleep(0)"#).exec_async().await.unwrap(); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_task_join_returns_results_positionally() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let (a, b, c): (i64, String, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
return task.join( |
||||||
|
function() return 1 end, |
||||||
|
function() os.sleep(0.01) return "two" end, |
||||||
|
function() return true end |
||||||
|
) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(a, 1); |
||||||
|
assert_eq!(b, "two"); |
||||||
|
assert!(c); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_task_join_runs_concurrently() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let start = Instant::now(); |
||||||
|
lua.load( |
||||||
|
r#" |
||||||
|
task.join( |
||||||
|
function() os.sleep(0.15) end, |
||||||
|
function() os.sleep(0.1) end, |
||||||
|
function() os.sleep(0.05) end |
||||||
|
) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
let elapsed = start.elapsed(); |
||||||
|
|
||||||
|
// Sequential execution would take ~0.3s; overlapped, the slowest task dominates.
|
||||||
|
assert!(elapsed >= Duration::from_millis(140), "finished too fast: {elapsed:?}"); |
||||||
|
assert!(elapsed < Duration::from_millis(280), "tasks did not overlap: {elapsed:?}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_task_join_rejects_non_function_argument() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
// A non-function argument gives a prefixed, positioned error, not mlua's
|
||||||
|
// bare "error converting Lua integer to function".
|
||||||
|
let err = lua |
||||||
|
.load(r#"task.join(function() end, 42)"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
let msg = err.to_string(); |
||||||
|
assert!(msg.contains("task.join: argument #2 must be a function"), "{msg}"); |
||||||
|
assert!(msg.contains("integer"), "{msg}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_os_sleep_rejects_non_number() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let err = lua.load(r#"os.sleep("soon")"#).exec_async().await.unwrap_err(); |
||||||
|
let msg = err.to_string(); |
||||||
|
assert!(msg.contains("os.sleep: duration must be a number"), "{msg}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_task_join_propagates_errors() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load(r#"task.join(function() error("boom") end, function() return 1 end)"#) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("boom")); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_in_memory_roundtrip() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let (name, id, count): (String, i64, i64) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)") |
||||||
|
db:execute("INSERT INTO t (name) VALUES (?)", {"alice"}) |
||||||
|
db:execute("INSERT INTO t (name) VALUES (?)", {"bob"}) |
||||||
|
local id = db:lastInsertRowid() |
||||||
|
local row = db:queryOne("SELECT name FROM t WHERE id = ?", {1}) |
||||||
|
local rows = db:query("SELECT * FROM t") |
||||||
|
db:close() |
||||||
|
return row.name, id, #rows |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(name, "alice"); |
||||||
|
assert_eq!(id, 2); |
||||||
|
assert_eq!(count, 2); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_binds_non_utf8_as_blob() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
// A non-UTF-8 / NUL-containing Lua string must bind and round-trip intact
|
||||||
|
// (as a BLOB) rather than failing the UTF-8 conversion.
|
||||||
|
let ok: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (id INTEGER PRIMARY KEY, data)") |
||||||
|
local blob = "\255\0\254bin" |
||||||
|
db:execute("INSERT INTO t (data) VALUES (?)", {blob}) |
||||||
|
local got = db:queryOne("SELECT data FROM t WHERE id = 1").data |
||||||
|
return got == blob |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_rejects_empty_and_multi_statements() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load(r#"local db = sqlite.connect(":memory:"); return db:execute(" ")"#) |
||||||
|
.eval_async::<mlua::Value>() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("no SQL statement"), "{err}"); |
||||||
|
|
||||||
|
// A comment-only statement has no runnable SQL either.
|
||||||
|
let err = lua |
||||||
|
.load(r#"local db = sqlite.connect(":memory:"); return db:query("-- only a comment")"#) |
||||||
|
.eval_async::<mlua::Value>() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("no SQL statement"), "{err}"); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (x)") |
||||||
|
return db:execute("INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)") |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async::<mlua::Value>() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("single SQL statement"), "{err}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_execute_batch_runs_all_statements() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let count: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:executeBatch([[ |
||||||
|
CREATE TABLE t (x); |
||||||
|
INSERT INTO t VALUES (1); |
||||||
|
INSERT INTO t VALUES (2); |
||||||
|
INSERT INTO t VALUES (3); |
||||||
|
]]) |
||||||
|
return db:queryOne("SELECT COUNT(*) AS n FROM t").n |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(count, 3); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_transaction_passes_through_return_values() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let (a, b): (i64, String) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (x)") |
||||||
|
return db:transaction(function() return 7, "eight" end) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(a, 7); |
||||||
|
assert_eq!(b, "eight"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_transaction_does_not_mask_original_error() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
// If the body fails AND rollback then also fails (here: the connection is
|
||||||
|
// closed inside the body), the caller must still see the body's error.
|
||||||
|
let err = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (x)") |
||||||
|
db:transaction(function() |
||||||
|
db:close() |
||||||
|
error("ORIGINAL ERROR") |
||||||
|
end) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec_async() |
||||||
|
.await |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("ORIGINAL ERROR"), "{err}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[tokio::test] |
||||||
|
async fn test_sqlite_transaction_commit_and_rollback() { |
||||||
|
let lua = lua(); |
||||||
|
|
||||||
|
let (after_commit, after_rollback): (i64, i64) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local db = sqlite.connect(":memory:") |
||||||
|
db:execute("CREATE TABLE t (x)") |
||||||
|
|
||||||
|
db:transaction(function() |
||||||
|
db:execute("INSERT INTO t VALUES (1)") |
||||||
|
end) |
||||||
|
local committed = db:queryOne("SELECT COUNT(*) AS n FROM t").n |
||||||
|
|
||||||
|
local ok, err = pcall(function() |
||||||
|
db:transaction(function() |
||||||
|
db:execute("INSERT INTO t VALUES (2)") |
||||||
|
error("nope") |
||||||
|
end) |
||||||
|
end) |
||||||
|
assert(not ok) |
||||||
|
assert(tostring(err):find("nope")) |
||||||
|
local rolled_back = db:queryOne("SELECT COUNT(*) AS n FROM t").n |
||||||
|
|
||||||
|
return committed, rolled_back |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval_async() |
||||||
|
.await |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
assert_eq!(after_commit, 1); |
||||||
|
// The failed transaction's insert must have been rolled back.
|
||||||
|
assert_eq!(after_rollback, 1); |
||||||
|
} |
||||||
@ -0,0 +1,226 @@ |
|||||||
|
//! Core environment tests: basic Lua evaluation, sandbox verification, and
|
||||||
|
//! checks that the stdlib extends rather than replaces the standard modules.
|
||||||
|
//!
|
||||||
|
//! Ported from flowbox-rt's fb_lua core_tests, with sandbox-verification
|
||||||
|
//! tests added for this project's sandbox (see src/sandbox.rs).
|
||||||
|
|
||||||
|
use super::assert_eq_f64; |
||||||
|
|
||||||
|
/// Smoke test for the environment setup
|
||||||
|
#[test] |
||||||
|
fn test_call_lua_function() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let chunk: mlua::Function = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
function(a, b) |
||||||
|
return a + b |
||||||
|
end |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let res: i32 = chunk.call((10, 20)).unwrap(); |
||||||
|
assert_eq!(res, 30); |
||||||
|
} |
||||||
|
|
||||||
|
/// dofile, loadfile and load (raw bytecode loading) are removed from the sandbox.
|
||||||
|
#[test] |
||||||
|
fn test_sandbox_chunk_loaders_removed() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
for name in ["dofile", "loadfile", "load"] { |
||||||
|
let is_nil: bool = lua.load(format!("return {name} == nil")).eval().unwrap(); |
||||||
|
assert!(is_nil, "global `{name}` should be nil in the sandbox"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// string.dump (bytecode producer, the counterpart of load) is removed.
|
||||||
|
#[test] |
||||||
|
fn test_sandbox_string_dump_removed() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let is_nil: bool = lua.load(r#"return string.dump == nil"#).eval().unwrap(); |
||||||
|
assert!(is_nil, "string.dump should be nil in the sandbox"); |
||||||
|
} |
||||||
|
|
||||||
|
/// io, package and debug libraries are not loaded at all.
|
||||||
|
#[test] |
||||||
|
fn test_sandbox_io_package_debug_not_loaded() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
for name in ["io", "package", "debug"] { |
||||||
|
let is_nil: bool = lua.load(format!("return {name} == nil")).eval().unwrap(); |
||||||
|
assert!(is_nil, "global `{name}` should be nil in the sandbox"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// collectgarbage only allows the "count" option; everything else raises
|
||||||
|
/// an error mentioning the sandbox.
|
||||||
|
#[test] |
||||||
|
fn test_sandbox_collectgarbage_count_only() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let count: f64 = lua.load(r#"return collectgarbage("count")"#).eval().unwrap(); |
||||||
|
assert!(count > 0.0, "collectgarbage(\"count\") should report heap KB, got {count}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return collectgarbage()"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("sandboxed"), "unexpected error: {err}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return collectgarbage("collect")"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("sandboxed"), "unexpected error: {err}"); |
||||||
|
} |
||||||
|
|
||||||
|
/// The os table only keeps the safe time functions plus our extensions.
|
||||||
|
#[test] |
||||||
|
fn test_sandbox_os_dangerous_functions_removed() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let os_table = lua.globals().get::<mlua::Table>("os").unwrap(); |
||||||
|
for name in ["execute", "exit", "getenv", "remove", "rename", "setlocale", "tmpname"] { |
||||||
|
assert!( |
||||||
|
!os_table.contains_key(name).unwrap(), |
||||||
|
"os.{name} should be removed from the sandbox" |
||||||
|
); |
||||||
|
} |
||||||
|
for name in ["date", "difftime", "time", "clock", "microtime", "sleep"] { |
||||||
|
assert!( |
||||||
|
os_table.contains_key(name).unwrap(), |
||||||
|
"os.{name} should be present in the sandbox" |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Verify that the math table was extended with custom functions,
|
||||||
|
/// not replaced - standard Lua math functions must still exist.
|
||||||
|
#[test] |
||||||
|
fn test_math_table_extended_not_replaced() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Test standard math functions still work
|
||||||
|
let result: f64 = lua.load(r#"return math.floor(3.7)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.ceil(3.2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 4.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.abs(-5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 5.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.sqrt(16)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 4.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.sin(0)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.cos(0)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 1.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.max(1, 5, 3)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 5.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.min(1, 5, 3)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 1.0); |
||||||
|
|
||||||
|
// Verify math.pi constant exists
|
||||||
|
let result: f64 = lua.load(r#"return math.pi"#).eval().unwrap(); |
||||||
|
assert!(result > 3.14 && result < 3.15); |
||||||
|
|
||||||
|
// Verify our custom functions are also present
|
||||||
|
let result: i64 = lua.load(r#"return math.round(3.7)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 4); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.round(3.1415, 2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.14); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(50, 0, 100)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 50); |
||||||
|
} |
||||||
|
|
||||||
|
/// Verify that the table module was extended with custom functions,
|
||||||
|
/// not replaced - standard Lua table functions must still exist.
|
||||||
|
#[test] |
||||||
|
fn test_table_module_extended_not_replaced() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Test standard table functions still work
|
||||||
|
// table.insert
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {1, 2, 3} |
||||||
|
table.insert(t, 4) |
||||||
|
return utils.toJSON(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, "[1,2,3,4]"); |
||||||
|
|
||||||
|
// table.remove
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {1, 2, 3, 4} |
||||||
|
table.remove(t, 2) |
||||||
|
return utils.toJSON(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, "[1,3,4]"); |
||||||
|
|
||||||
|
// table.concat
|
||||||
|
let result: String = lua.load(r#"return table.concat({"a", "b", "c"}, "-")"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "a-b-c"); |
||||||
|
|
||||||
|
// table.sort
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {3, 1, 4, 1, 5, 9} |
||||||
|
table.sort(t) |
||||||
|
return utils.toJSON(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, "[1,1,3,4,5,9]"); |
||||||
|
|
||||||
|
// table.unpack
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {10, 20, 30} |
||||||
|
local a, b, c = table.unpack(t) |
||||||
|
return a + b + c |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 60); |
||||||
|
|
||||||
|
// Verify our custom functions are also present
|
||||||
|
let _: () = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local ro = table.readonly({x = 1}) |
||||||
|
assert(ro.x == 1) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec() |
||||||
|
.unwrap(); |
||||||
|
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local merged = table.merge({a = 1}, {b = 2}) |
||||||
|
return utils.toJSON(merged) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result.contains("\"a\":1") && result.contains("\"b\":2")); |
||||||
|
} |
||||||
@ -0,0 +1,398 @@ |
|||||||
|
//! Tests for the math stdlib extensions (lua/stdlib/math.lua).
|
||||||
|
|
||||||
|
use super::assert_eq_f64; |
||||||
|
use mlua::prelude::LuaValue; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_clamp() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Simple, integers
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(3, 10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 10); // low
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(3, nil, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 3); // low-unbounded
|
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(15, 10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 15); // inside
|
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(25, 10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 20); // high
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(25, 10, nil)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 25); // high-unbounded
|
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(999, nil, nil)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 999); // unbounded
|
||||||
|
|
||||||
|
// Floats
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(3.5, 10.5, 20.5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 10.5); // low
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(3.5, nil, 20.5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.5); // low-unbounded
|
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(15.5, 10.5, 20.5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 15.5); // inside
|
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(25.5, 10.5, 20.5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 20.5); // high
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(25.5, 10.5, nil)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 25.5); // high-unbounded
|
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.clamp(999.9, nil, nil)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 999.9); // unbounded
|
||||||
|
|
||||||
|
// Check some negative numbers
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(3, -10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 3); // low
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(-15, -10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -10); // inside
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(-25, -10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -10); // high
|
||||||
|
let result: i64 = lua.load(r#"return math.clamp(25, -10, 20)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 20); // high
|
||||||
|
|
||||||
|
assert!(lua.load(r#"return math.clamp(nil, 1, 2)"#).eval::<LuaValue>().is_err()); |
||||||
|
assert!(lua.load(r#"return math.clamp(1, "1", 2)"#).eval::<LuaValue>().is_err()); |
||||||
|
assert!(lua.load(r#"return math.clamp(1, "foo", 2)"#).eval::<LuaValue>().is_err()); |
||||||
|
assert!(lua.load(r#"return math.clamp(1, 1, "bar")"#).eval::<LuaValue>().is_err()); |
||||||
|
assert!(lua.load(r#"return math.clamp(1, 1, "2")"#).eval::<LuaValue>().is_err()); |
||||||
|
|
||||||
|
// Crossed bounds (min > max) is a programming error, not a valid interval
|
||||||
|
let err = lua.load(r#"return math.clamp(5, 10, 0)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("greater than")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_round() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Round positive numbers
|
||||||
|
let result: i64 = lua.load(r#"return math.round(3.4)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 3); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.round(3.5)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 4); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.round(3.9)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 4); |
||||||
|
|
||||||
|
// Round negative numbers
|
||||||
|
let result: i64 = lua.load(r#"return math.round(-3.4)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -3); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.round(-3.5)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -4); |
||||||
|
|
||||||
|
// Round whole numbers (should stay the same)
|
||||||
|
let result: i64 = lua.load(r#"return math.round(5)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 5); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.round(0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
|
||||||
|
// Round with a non-number should error
|
||||||
|
let err = lua.load(r#"return math.round("not a number")"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("non-number")); |
||||||
|
|
||||||
|
// NaN must error, not silently become 0
|
||||||
|
let err = lua.load(r#"return math.round(0/0)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("finite")); |
||||||
|
|
||||||
|
// Infinity must error (out of integer range)
|
||||||
|
assert!(lua.load(r#"return math.round(math.huge)"#).exec().is_err()); |
||||||
|
assert!(lua.load(r#"return math.round(-math.huge)"#).exec().is_err()); |
||||||
|
|
||||||
|
// Out of i64 range must error
|
||||||
|
assert!(lua.load(r#"return math.round(1e19)"#).exec().is_err()); |
||||||
|
assert!(lua.load(r#"return math.round(-1e19)"#).exec().is_err()); |
||||||
|
|
||||||
|
// Half away from zero at exactly +-0.5
|
||||||
|
let result: i64 = lua.load(r#"return math.round(0.5)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 1); |
||||||
|
let result: i64 = lua.load(r#"return math.round(-0.5)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -1); |
||||||
|
|
||||||
|
// The naive floor(value + 0.5) trap: 0.49999999999999994 + 0.5 == 1.0 in f64,
|
||||||
|
// but the nearest integer is 0
|
||||||
|
let result: i64 = lua.load(r#"return math.round(0.49999999999999994)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
let result: i64 = lua.load(r#"return math.round(-0.49999999999999994)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
|
||||||
|
// Integer-valued floats at the edge of f64 precision (2^53)
|
||||||
|
let result: i64 = lua.load(r#"return math.round(2.0^53 - 1.0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 9007199254740991); |
||||||
|
let result: i64 = lua.load(r#"return math.round(2.0^53)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 9007199254740992); |
||||||
|
|
||||||
|
// -2^63 is exactly representable as a float and is a valid Lua integer...
|
||||||
|
let result: bool = lua.load(r#"return math.round(-2.0^63) == math.mininteger"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// ...but 2^63 is one above math.maxinteger and must error
|
||||||
|
assert!(lua.load(r#"return math.round(2.0^63)"#).exec().is_err()); |
||||||
|
} |
||||||
|
|
||||||
|
//noinspection RsApproxConstant
|
||||||
|
#[test] |
||||||
|
fn test_round_to_places() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// roundn was merged into round - it must no longer exist
|
||||||
|
let result: bool = lua.load(r#"return math.roundn == nil"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// places = 0 behaves like the one-argument form and returns an integer
|
||||||
|
let result: String = lua.load(r#"return math.type(math.round(3.1415, 0))"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "integer"); |
||||||
|
let result: i64 = lua.load(r#"return math.round(3.7, 0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 4); |
||||||
|
|
||||||
|
// places > 0 rounds to N decimal places and returns a float
|
||||||
|
let result: f64 = lua.load(r#"return math.round(3.1415, 1)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.1); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.round(3.1415, 3)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.142); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.round(2.567, 2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 2.57); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return math.type(math.round(3.1415, 2))"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "float"); |
||||||
|
|
||||||
|
// Integer input with places > 0 returns the same value as a float
|
||||||
|
let result: String = lua.load(r#"return math.type(math.round(5, 2))"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "float"); |
||||||
|
let result: f64 = lua.load(r#"return math.round(5, 2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 5.0); |
||||||
|
|
||||||
|
// Negative numbers round half away from zero
|
||||||
|
let result: f64 = lua.load(r#"return math.round(-2.345, 2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, -2.35); |
||||||
|
|
||||||
|
// Huge values must not overflow to infinity via the 10^places multiplier
|
||||||
|
let result: f64 = lua.load(r#"return math.round(1.5e308, 2)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 1.5e308); |
||||||
|
|
||||||
|
// Huge places are a no-op (cannot change any f64), not an overflow
|
||||||
|
let result: f64 = lua.load(r#"return math.round(12.5, 400)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 12.5); |
||||||
|
|
||||||
|
// Tiny values still round correctly with large places
|
||||||
|
let result: f64 = lua.load(r#"return math.round(1.23e-300, 300)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 1.0e-300); |
||||||
|
|
||||||
|
// Places given as an integral float is accepted
|
||||||
|
let result: f64 = lua.load(r#"return math.round(3.14159, 2.0)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 3.14); |
||||||
|
|
||||||
|
// Non-integral places must error
|
||||||
|
assert!(lua.load(r#"return math.round(3.14, 2.5)"#).exec().is_err()); |
||||||
|
|
||||||
|
// Negative places must error with a clear message
|
||||||
|
let err = lua.load(r#"return math.round(3.14, -2)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("negative")); |
||||||
|
|
||||||
|
// NaN must error
|
||||||
|
let err = lua.load(r#"return math.round(0/0, 2)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("finite")); |
||||||
|
|
||||||
|
// Infinity must error
|
||||||
|
assert!(lua.load(r#"return math.round(math.huge, 2)"#).exec().is_err()); |
||||||
|
|
||||||
|
// Non-number must error
|
||||||
|
let err = lua.load(r#"return math.round("foo", 2)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("non-number")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_is_finite() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Regular numbers are finite
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(42)"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(-3.14)"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(0)"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// Infinity is not finite
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(math.huge)"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(-math.huge)"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
// NaN is not finite (0/0 produces NaN)
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(0/0)"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
// Non-numbers return false
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite("hello")"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(nil)"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite({})"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
// Very large numbers are still finite
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(1e308)"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// But overflow to infinity is not
|
||||||
|
let result: bool = lua.load(r#"return math.isFinite(1e309)"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_sign() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Positive numbers return 1
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(42)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 1); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(0.001)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 1); |
||||||
|
|
||||||
|
// Negative numbers return -1
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(-42)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -1); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(-0.001)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -1); |
||||||
|
|
||||||
|
// Zero returns 0
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
|
||||||
|
// Negative zero is still zero
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(-0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
|
||||||
|
// Positive infinity returns 1
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(math.huge)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 1); |
||||||
|
|
||||||
|
// Negative infinity returns -1
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(-math.huge)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, -1); |
||||||
|
|
||||||
|
// NaN returns 0 (NaN is not > 0 and not < 0)
|
||||||
|
let result: i64 = lua.load(r#"return math.sign(0/0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 0); |
||||||
|
|
||||||
|
// Error on non-number
|
||||||
|
let err = lua.load(r#"return math.sign("foo")"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("Non-number")); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_scale() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Scale 0-100 to 0-1
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(50, 0, 100, 0, 1)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.5); |
||||||
|
|
||||||
|
// Scale 0-100 to 0-10
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(25, 0, 100, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 2.5); |
||||||
|
|
||||||
|
// Scale with offset ranges
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(15, 10, 20, 100, 200)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 150.0); |
||||||
|
|
||||||
|
// Scale to inverted range
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(0, 0, 100, 100, 0)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 100.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(100, 0, 100, 100, 0)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
// Value outside input range (no clamp)
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(150, 0, 100, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 15.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(-50, 0, 100, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, -5.0); |
||||||
|
|
||||||
|
// Value outside input range with clamp=true
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(150, 0, 100, 0, 10, true)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 10.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(-50, 0, 100, 0, 10, true)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
// Clamp with inverted output range
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(150, 0, 100, 10, 0, true)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(-50, 0, 100, 10, 0, true)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 10.0); |
||||||
|
|
||||||
|
// Clamp=false (explicit) behaves same as default
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(150, 0, 100, 0, 10, false)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 15.0); |
||||||
|
|
||||||
|
// Zero-width output range is allowed (always returns outMin)
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(50, 0, 100, 5, 5)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 5.0); |
||||||
|
|
||||||
|
// Boundary values
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(0, 0, 100, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(100, 0, 100, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 10.0); |
||||||
|
|
||||||
|
// Negative input range
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(-50, -100, 0, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 5.0); |
||||||
|
|
||||||
|
// Inverted input range (inMin > inMax)
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(75, 100, 0, 0, 10)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 2.5); |
||||||
|
|
||||||
|
// Error on zero-width input range (division by zero)
|
||||||
|
let err = lua.load(r#"return math.scale(50, 100, 100, 0, 10)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("Input range cannot be zero")); |
||||||
|
|
||||||
|
// Error on non-number value
|
||||||
|
let err = lua.load(r#"return math.scale("foo", 0, 100, 0, 10)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("Non-number")); |
||||||
|
|
||||||
|
// Error on non-number range
|
||||||
|
let err = lua.load(r#"return math.scale(50, "a", 100, 0, 10)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("Non-number")); |
||||||
|
|
||||||
|
// Infinity input produces infinity output (no clamp)
|
||||||
|
let result: bool = lua |
||||||
|
.load(r#"return math.scale(math.huge, 0, 100, 0, 10) == math.huge"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// Infinity input with clamp gets clamped
|
||||||
|
let result: f64 = lua.load(r#"return math.scale(math.huge, 0, 100, 0, 10, true)"#).eval().unwrap(); |
||||||
|
assert_eq_f64!(result, 10.0); |
||||||
|
|
||||||
|
let result: f64 = lua |
||||||
|
.load(r#"return math.scale(-math.huge, 0, 100, 0, 10, true)"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq_f64!(result, 0.0); |
||||||
|
|
||||||
|
// NaN input produces NaN output
|
||||||
|
let result: bool = lua |
||||||
|
.load(r#"local r = math.scale(0/0, 0, 100, 0, 10); return r ~= r"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); // NaN ~= NaN is true
|
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
//! Tests for the Lua stdlib, ported from flowbox-rt's fb_lua `lua_tests` and
|
||||||
|
//! adapted to this project's sandbox and stdlib.
|
||||||
|
|
||||||
|
mod async_tests; |
||||||
|
mod core_tests; |
||||||
|
mod math_tests; |
||||||
|
mod os_tests; |
||||||
|
mod table_tests; |
||||||
|
mod utils_tests; |
||||||
|
|
||||||
|
use mlua::Lua; |
||||||
|
|
||||||
|
/// A fresh sandboxed Lua with the full stdlib installed - the same state scripts get.
|
||||||
|
pub(crate) fn lua() -> Lua { |
||||||
|
crate::sandbox::create_sandboxed_lua().unwrap() |
||||||
|
} |
||||||
|
|
||||||
|
/// Compare two JSON strings as parsed values (key order independent).
|
||||||
|
#[allow(dead_code)] |
||||||
|
pub(crate) fn json_eq(a: &str, b: &str) -> bool { |
||||||
|
let a: serde_json::Value = serde_json::from_str(a).unwrap(); |
||||||
|
let b: serde_json::Value = serde_json::from_str(b).unwrap(); |
||||||
|
a == b |
||||||
|
} |
||||||
|
|
||||||
|
/// Assert that two f64 values are equal within an epsilon (default `f64::EPSILON`).
|
||||||
|
macro_rules! assert_eq_f64 { |
||||||
|
($a:expr, $b:expr) => { |
||||||
|
assert_eq_f64!($a, $b, f64::EPSILON) |
||||||
|
}; |
||||||
|
($a:expr, $b:expr, $eps:expr) => {{ |
||||||
|
let (a, b): (f64, f64) = ($a, $b); |
||||||
|
let eps: f64 = $eps; |
||||||
|
assert!( |
||||||
|
(a - b).abs() <= eps, |
||||||
|
"assertion failed: `(left !== right)` (left: `{a:?}`, right: `{b:?}`, epsilon: `{eps:?}`, diff: `{:?}`)", |
||||||
|
(a - b).abs() |
||||||
|
); |
||||||
|
}}; |
||||||
|
} |
||||||
|
pub(crate) use assert_eq_f64; |
||||||
@ -0,0 +1,173 @@ |
|||||||
|
//! Tests for the sandboxed `os` table.
|
||||||
|
//!
|
||||||
|
//! Ported from flowbox-rt's fb_lua os_tests. Unlike flowbox, this project
|
||||||
|
//! keeps the real Lua 5.5 os.date/os.time/os.difftime (flowbox used a
|
||||||
|
//! chrono-based shim), so a few edge cases differ - see individual tests.
|
||||||
|
//! os.sleep is async here and is covered by async_tests, not this file.
|
||||||
|
|
||||||
|
use std::time::{Duration, Instant}; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_time_now() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return os.time()"#).eval().unwrap(); |
||||||
|
let now = chrono::Utc::now().timestamp(); |
||||||
|
assert!((result - now).abs() <= 5, "os.time() = {result}, expected ~{now}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_time_from_table() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Round trip through local time - independent of the host timezone
|
||||||
|
let ok: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = os.time({year = 2020, month = 1, day = 15, hour = 10, min = 30, sec = 20}) |
||||||
|
local d = os.date("*t", t) |
||||||
|
return d.year == 2020 and d.month == 1 and d.day == 15 |
||||||
|
and d.hour == 10 and d.min == 30 and d.sec == 20 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
|
||||||
|
// Out-of-range fields are normalized like mktime; hour defaults to 12
|
||||||
|
let ok: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a = os.time({year = 2020, month = 14, day = 1}) |
||||||
|
local b = os.time({year = 2021, month = 2, day = 1}) |
||||||
|
return a == b |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
|
||||||
|
// The normalized fields are written back into the table
|
||||||
|
let ok: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local tbl = {year = 2020, month = 14, day = 1} |
||||||
|
os.time(tbl) |
||||||
|
return tbl.year == 2021 and tbl.month == 2 and tbl.day == 1 |
||||||
|
and tbl.hour == 12 and tbl.wday == 2 -- 2021-02-01 was a Monday |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
|
||||||
|
// Missing mandatory field is an error
|
||||||
|
let ok: bool = lua.load(r#"return (pcall(os.time, {year = 2020}))"#).eval().unwrap(); |
||||||
|
assert!(!ok); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_date_format() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return os.date("!%Y-%m-%dT%H:%M:%S", 1000000000)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "2001-09-09T01:46:40"); |
||||||
|
|
||||||
|
// Epoch
|
||||||
|
let result: String = lua.load(r#"return os.date("!%Y-%m-%d %H:%M:%S", 0)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "1970-01-01 00:00:00"); |
||||||
|
|
||||||
|
// Stock Lua difference: flowbox's chrono shim truncated float time values;
|
||||||
|
// real Lua 5.5 requires an integer-representable time and raises an error.
|
||||||
|
let ok: bool = lua.load(r#"return (pcall(os.date, "!%Y-%m-%d", 0.9))"#).eval().unwrap(); |
||||||
|
assert!(!ok, "os.date with a fractional time value should raise in stock Lua"); |
||||||
|
|
||||||
|
// Default format is %c, applied to the current time
|
||||||
|
let result: String = lua.load(r#"return os.date()"#).eval().unwrap(); |
||||||
|
assert!(!result.is_empty()); |
||||||
|
|
||||||
|
// Invalid format specifier is an error, not garbage output
|
||||||
|
let ok: bool = lua.load(r#"return (pcall(os.date, "%Q"))"#).eval().unwrap(); |
||||||
|
assert!(!ok); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_date_table() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// 2001-09-09T01:46:40Z was a Sunday, day 252 of the year
|
||||||
|
let ok: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local d = os.date("!*t", 1000000000) |
||||||
|
return d.year == 2001 and d.month == 9 and d.day == 9 |
||||||
|
and d.hour == 1 and d.min == 46 and d.sec == 40 |
||||||
|
and d.wday == 1 and d.yday == 252 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(ok); |
||||||
|
} |
||||||
|
|
||||||
|
/// os.clock is replaced with monotonic wall time since state creation.
|
||||||
|
#[test] |
||||||
|
fn test_os_clock() { |
||||||
|
let start = Instant::now(); |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_millis(30)); |
||||||
|
|
||||||
|
let clock: f64 = lua.load(r#"return os.clock()"#).eval().unwrap(); |
||||||
|
assert!( |
||||||
|
(start.elapsed().as_secs_f64() - clock).abs() < 0.1, |
||||||
|
"Clock function works" |
||||||
|
); |
||||||
|
|
||||||
|
// 200 ms elapses, so it should be reported accurately
|
||||||
|
std::thread::sleep(Duration::from_millis(200)); |
||||||
|
let clock2: f64 = lua.load(r#"return os.clock()"#).eval().unwrap(); |
||||||
|
|
||||||
|
assert!((clock2 - clock - 0.2) < 0.03, "Clock tracks time"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_difftime() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return os.difftime(10, 4)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 6.0); |
||||||
|
|
||||||
|
let ok: bool = lua.load(r#"return (pcall(os.difftime, 10))"#).eval().unwrap(); |
||||||
|
assert!(!ok); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_missing_dangerous_functions() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let os_table = lua.globals().get::<mlua::Table>("os").unwrap(); |
||||||
|
for name in ["execute", "exit", "getenv", "remove", "rename", "setlocale", "tmpname"] { |
||||||
|
assert!( |
||||||
|
!os_table.contains_key(name).unwrap(), |
||||||
|
"os.{name} should be removed from the sandbox" |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_os_microtime() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let result1: f64 = lua.load(r#"return os.microtime()"#).eval().unwrap(); |
||||||
|
std::thread::sleep(Duration::from_millis(100)); |
||||||
|
let result2: f64 = lua.load(r#"return os.microtime()"#).eval().unwrap(); |
||||||
|
assert!( |
||||||
|
(result2 - result1 - 0.1).abs() < 0.03, |
||||||
|
"os.microtime() should return fractional seconds" |
||||||
|
); |
||||||
|
|
||||||
|
// It is a Unix timestamp
|
||||||
|
let now = chrono::Utc::now().timestamp() as f64; |
||||||
|
assert!((result2 - now).abs() <= 5.0, "os.microtime() = {result2}, expected ~{now}"); |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,813 @@ |
|||||||
|
//! Tests for the `utils` global: NULL sentinel, isNull, toJSON/fromJSON, dump
|
||||||
|
//! (Rust side) and try, tryn (Lua side, lua/stdlib/utils.lua).
|
||||||
|
//!
|
||||||
|
//! Ported from flowbox-rt fb_lua utils_tests. `utils.dump` is the flowbox Rust
|
||||||
|
//! implementation (src/stdlib/lua_dump.rs), so the dump tests match flowbox's.
|
||||||
|
|
||||||
|
use super::json_eq; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_dump() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let dump = |code: &str| -> String { |
||||||
|
lua.load(format!("return utils.dump({})", code)) |
||||||
|
.eval::<String>() |
||||||
|
.unwrap() |
||||||
|
}; |
||||||
|
|
||||||
|
// nil
|
||||||
|
assert_eq!(dump("nil"), "nil"); |
||||||
|
|
||||||
|
// booleans
|
||||||
|
assert_eq!(dump("true"), "true"); |
||||||
|
assert_eq!(dump("false"), "false"); |
||||||
|
|
||||||
|
// integers
|
||||||
|
assert_eq!(dump("1"), "1"); |
||||||
|
assert_eq!(dump("0"), "0"); |
||||||
|
assert_eq!(dump("-42"), "-42"); |
||||||
|
|
||||||
|
// floats - whole numbers show .0 (Lua 5.4 tostring)
|
||||||
|
assert_eq!(dump("1.0"), "1.0"); |
||||||
|
assert_eq!(dump("0.0"), "0.0"); |
||||||
|
assert_eq!(dump("-5.0"), "-5.0"); |
||||||
|
|
||||||
|
// floats - with fractional part
|
||||||
|
assert_eq!(dump("1.5"), "1.5"); |
||||||
|
assert_eq!(dump("-3.14159"), "-3.14159"); |
||||||
|
assert_eq!(dump("0.001"), "0.001"); |
||||||
|
|
||||||
|
// strings are quoted and escaped JSON-style
|
||||||
|
assert_eq!(dump("'hello'"), "\"hello\""); |
||||||
|
assert_eq!(dump("''"), "\"\""); // empty
|
||||||
|
assert_eq!(dump("'with spaces'"), "\"with spaces\""); |
||||||
|
assert_eq!(dump("'special\\nchars\\ttab'"), "\"special\\nchars\\ttab\""); |
||||||
|
|
||||||
|
// empty table
|
||||||
|
assert_eq!(dump("{}"), "{}"); |
||||||
|
|
||||||
|
// simple table with string key
|
||||||
|
assert_eq!(dump("{foo=1}"), "{foo=1}"); |
||||||
|
assert_eq!(dump("{_foo=1}"), "{_foo=1}"); |
||||||
|
assert_eq!(dump("{_1=1}"), "{_1=1}"); |
||||||
|
// a string key that does not look like an identifier is bracketed and quoted
|
||||||
|
assert_eq!(dump("{[\"1\"]=1}"), "{[\"1\"]=1}"); |
||||||
|
|
||||||
|
// nested table
|
||||||
|
assert_eq!(dump("{foo={bar=1}}"), "{foo={bar=1}}"); |
||||||
|
|
||||||
|
// array-style table (numeric keys)
|
||||||
|
assert_eq!(dump("{10, 20, 30}"), "{10, 20, 30}"); |
||||||
|
|
||||||
|
// mixed keys - order may vary, so check by substrings
|
||||||
|
let dumped = dump("{foo=1, [999]='a'}"); |
||||||
|
assert!(dumped.contains("foo=1"), "got: {dumped}"); |
||||||
|
assert!(dumped.contains("[999]=\"a\""), "got: {dumped}"); |
||||||
|
|
||||||
|
// a table with both an array part and hash keys dumps both
|
||||||
|
assert_eq!(dump("{10, 20, foo='hash'}"), "{10, 20, foo=\"hash\"}"); |
||||||
|
|
||||||
|
// function
|
||||||
|
assert_eq!(dump("function() end"), "<Function>"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_dump_deep_nesting() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Recursion limit: tables at depth > 20 are rendered as <TRUNCATED>
|
||||||
|
// Build a table nested 25 levels deep - truncation happens at depth 21
|
||||||
|
let deep_result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {val="leaf"} |
||||||
|
for i = 1, 25 do |
||||||
|
t = {nested=t} |
||||||
|
end |
||||||
|
return utils.dump(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
// 20 levels of {nested=, then the innermost table's value truncated at depth 21
|
||||||
|
let expected = format!("{}{{nested=<TRUNCATED>}}{}", "{nested=".repeat(20), "}".repeat(20)); |
||||||
|
assert_eq!(deep_result, expected); |
||||||
|
|
||||||
|
// A table just within the limit still dumps fully
|
||||||
|
let shallow_result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {val="leaf"} |
||||||
|
for i = 1, 5 do |
||||||
|
t = {nested=t} |
||||||
|
end |
||||||
|
return utils.dump(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
let expected = format!("{}{{val=\"leaf\"}}{}", "{nested=".repeat(5), "}".repeat(5)); |
||||||
|
assert_eq!(shallow_result, expected); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_dump_circular() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// There is no cycle detection; the depth limit terminates the recursion,
|
||||||
|
// so a self-referencing table dumps 21 levels and then truncates
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {} |
||||||
|
t.self = t |
||||||
|
return utils.dump(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
let expected = format!("{}<TRUNCATED>{}", "{self=".repeat(21), "}".repeat(21)); |
||||||
|
assert_eq!(result, expected); |
||||||
|
|
||||||
|
// The same table appearing twice as a sibling dumps twice
|
||||||
|
let result: String = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local shared = {x = 1} |
||||||
|
return utils.dump({shared, shared}) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, "{{x=1}, {x=1}}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_to_json() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Primitives
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(nil)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "null"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(true)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "true"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(false)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "false"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(42)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "42"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(3.14)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "3.14"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON("hello")"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "\"hello\""); |
||||||
|
|
||||||
|
// Empty table serializes as an empty object
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{}"); |
||||||
|
|
||||||
|
// Array-style table
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({1, 2, 3})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "[1,2,3]"); |
||||||
|
|
||||||
|
// Object-style table
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({a=1})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{\"a\":1}"); |
||||||
|
|
||||||
|
// Nested structure
|
||||||
|
let result: String = lua |
||||||
|
.load(r#"return utils.toJSON({nested={value=42}})"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, "{\"nested\":{\"value\":42}}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_to_json_mixed_and_integer_keys() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// A table with both a sequence part and hash keys is serialized as an
|
||||||
|
// object; integer keys become string keys
|
||||||
|
let result: String = lua |
||||||
|
.load(r#"return utils.toJSON({1, 2, x = 3})"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(json_eq(&result, r#"{"1":1,"2":2,"x":3}"#), "got: {result}"); |
||||||
|
|
||||||
|
// Non-sequential integer keys become string object keys
|
||||||
|
let result: String = lua |
||||||
|
.load(r#"return utils.toJSON({a = 1, [10] = "x"})"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(json_eq(&result, r#"{"a":1,"10":"x"}"#), "got: {result}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_to_json_pretty() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Second argument enables pretty-printing
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({a=1}, true)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{\n \"a\": 1\n}"); |
||||||
|
|
||||||
|
// Explicit false behaves like the default compact output
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({a=1}, false)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{\"a\":1}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_to_json_unserializable_errors() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Unserializable values must raise an error instead of silently returning "null"
|
||||||
|
let err = lua.load(r#"return utils.toJSON(function() end)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("toJSON"), "got: {err}"); |
||||||
|
assert!(err.to_string().contains("function"), "got: {err}"); |
||||||
|
|
||||||
|
// A single bad value must not silently discard the whole structure
|
||||||
|
let err = lua |
||||||
|
.load(r#"return utils.toJSON({x = 1, f = function() end})"#) |
||||||
|
.exec() |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("toJSON"), "got: {err}"); |
||||||
|
|
||||||
|
// Self-referencing tables cannot be serialized (caught by the depth limit)
|
||||||
|
let err = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local t = {} |
||||||
|
t.self = t |
||||||
|
return utils.toJSON(t) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.exec() |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("toJSON"), "got: {err}"); |
||||||
|
assert!(err.to_string().contains("nesting too deep"), "got: {err}"); |
||||||
|
|
||||||
|
// NaN and Infinity have no JSON representation
|
||||||
|
let err = lua.load(r#"return utils.toJSON(0/0)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("NaN"), "got: {err}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return utils.toJSON(math.huge)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("Infinity"), "got: {err}"); |
||||||
|
|
||||||
|
// Only string and integer keys can become JSON object keys
|
||||||
|
let err = lua.load(r#"return utils.toJSON({[true] = 1})"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("object key"), "got: {err}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return utils.toJSON({[1.5] = 1})"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("object key"), "got: {err}"); |
||||||
|
|
||||||
|
// An integer key that stringifies onto an existing string key must error
|
||||||
|
// rather than silently dropping one of the two values.
|
||||||
|
let err = lua |
||||||
|
.load(r#"return utils.toJSON({[1] = "int", ["1"] = "str"})"#) |
||||||
|
.exec() |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("duplicate object key"), "got: {err}"); |
||||||
|
|
||||||
|
// A non-UTF-8 string value gets a clear prefixed error, not mlua's bare
|
||||||
|
// "error converting Lua string to &str".
|
||||||
|
let err = lua.load(r#"return utils.toJSON("\255bad")"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("utils.toJSON"), "got: {err}"); |
||||||
|
assert!(err.to_string().contains("UTF-8"), "got: {err}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_null_sentinel() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// utils.NULL exists and is distinct from nil
|
||||||
|
let result: bool = lua.load(r#"return utils.NULL ~= nil"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// isNull identifies the sentinel and nothing else
|
||||||
|
let result: bool = lua.load(r#"return utils.isNull(utils.NULL)"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
return utils.isNull(nil) or utils.isNull(0) or utils.isNull("") |
||||||
|
or utils.isNull({}) or utils.isNull(false) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
// JSON null deserializes to the sentinel - consistently at any nesting level
|
||||||
|
let result: bool = lua.load(r#"return utils.isNull(utils.fromJSON("null"))"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local obj = utils.fromJSON('{"a": null, "b": 1}') |
||||||
|
return utils.isNull(obj.a) and obj.a ~= nil and obj.b == 1 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// Nulls in arrays preserve the array length
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local arr = utils.fromJSON('[1, null, 3]') |
||||||
|
return #arr == 3 and utils.isNull(arr[2]) and arr[1] == 1 and arr[3] == 3 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// The sentinel serializes back to JSON null
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON(utils.NULL)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "null"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.toJSON({a = utils.NULL})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{\"a\":null}"); |
||||||
|
|
||||||
|
// dump renders the sentinel as NULL, bare and inside tables
|
||||||
|
// (other light userdata dumps as <LightUserData>)
|
||||||
|
let result: String = lua.load(r#"return utils.dump(utils.NULL)"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "NULL"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.dump({a = utils.NULL})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{a=NULL}"); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.dump({1, utils.NULL, 3})"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "{1, NULL, 3}"); |
||||||
|
} |
||||||
|
|
||||||
|
/// utils.NULL is mlua's own null sentinel (Value::NULL, a null-pointer light
|
||||||
|
/// userdata), so nulls interoperate with mlua's serde layer in both directions.
|
||||||
|
#[test] |
||||||
|
fn test_null_sentinel_matches_mlua() { |
||||||
|
use mlua::LuaSerdeExt; |
||||||
|
|
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
let null_val: mlua::Value = lua.load(r#"return utils.NULL"#).eval().unwrap(); |
||||||
|
assert_eq!(null_val, mlua::Value::NULL); |
||||||
|
|
||||||
|
// a null produced by mlua (lua.null()) is recognized by utils.isNull
|
||||||
|
let is_null: bool = lua |
||||||
|
.globals() |
||||||
|
.get::<mlua::Table>("utils") |
||||||
|
.unwrap() |
||||||
|
.get::<mlua::Function>("isNull") |
||||||
|
.unwrap() |
||||||
|
.call(lua.null()) |
||||||
|
.unwrap(); |
||||||
|
assert!(is_null); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_utils_table_extensible() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// utils is a plain global table just like math and table - scripts can extend it
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
utils.myHelper = function() return 41 + 1 end |
||||||
|
return utils.myHelper() |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 42); |
||||||
|
|
||||||
|
// and the built-in functions are of course still there
|
||||||
|
let result: bool = lua.load(r#"return type(utils.try) == "function""#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_try_tryn_arg_validation() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// The callback must be a function, rejected with a clear message
|
||||||
|
let err = lua.load(r#"return utils.try(5)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("not a function"), "got: {err}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return utils.try(nil)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("not a function"), "got: {err}"); |
||||||
|
|
||||||
|
let err = lua.load(r#"return utils.tryn(2, 5)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("not a function"), "got: {err}"); |
||||||
|
|
||||||
|
// expectedCount must be a non-negative integer
|
||||||
|
let err = lua.load(r#"return utils.tryn(2.5, function() end)"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("integer"), "got: {err}"); |
||||||
|
|
||||||
|
assert!(lua.load(r#"return utils.tryn(-1, function() end)"#).exec().is_err()); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_try_error_values() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// A thrown table is passed through as-is (pcall semantics), so scripts can
|
||||||
|
// attach structured data to errors
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local v, err = utils.try(function() error({code = 42}) end) |
||||||
|
return v == nil and type(err) == "table" and err.code == 42 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
// A nil error value still yields a non-nil err, so `if err` always detects failure
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local v, err = utils.try(function() error() end) |
||||||
|
return err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a, err = utils.tryn(1, function() error() end) |
||||||
|
return err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_tryn_return_counts() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// tryn returns exactly expectedCount + 1 values (the last one is the error slot),
|
||||||
|
// both on success and on failure
|
||||||
|
let result: i64 = lua |
||||||
|
.load(r#"return select('#', utils.tryn(2, function() return 1 end))"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 3); |
||||||
|
|
||||||
|
let result: i64 = lua |
||||||
|
.load(r#"return select('#', utils.tryn(2, function() error("x") end))"#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 3); |
||||||
|
|
||||||
|
// expectedCount = 0 gives just the error slot
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local err = utils.tryn(0, function() return 1, 2 end) |
||||||
|
return err == nil and select('#', utils.tryn(0, function() end)) == 1 |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_tryn_count_cap() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Excessive expected_count must error early instead of allocating huge buffers
|
||||||
|
let err = lua |
||||||
|
.load(r#"return utils.tryn(1000000, function() return 1 end)"#) |
||||||
|
.exec() |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("at most"), "got: {err}"); |
||||||
|
|
||||||
|
let err = lua |
||||||
|
.load(r#"return utils.tryn(65, function() return 1 end)"#) |
||||||
|
.exec() |
||||||
|
.unwrap_err(); |
||||||
|
assert!(err.to_string().contains("at most"), "got: {err}"); |
||||||
|
|
||||||
|
// The maximum allowed count still works
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local r = {utils.tryn(64, function() return 42 end)} |
||||||
|
return r[1] |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 42); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_from_json() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Primitives - JSON null becomes the utils.NULL sentinel, not nil
|
||||||
|
lua.load(r#"assert(utils.isNull(utils.fromJSON("null")))"#).exec().unwrap(); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return utils.fromJSON("true")"#).eval().unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: bool = lua.load(r#"return utils.fromJSON("false")"#).eval().unwrap(); |
||||||
|
assert!(!result); |
||||||
|
|
||||||
|
let result: i64 = lua.load(r#"return utils.fromJSON("42")"#).eval().unwrap(); |
||||||
|
assert_eq!(result, 42); |
||||||
|
|
||||||
|
// Whole JSON numbers arrive as Lua integers, fractional ones as floats
|
||||||
|
let result: bool = lua |
||||||
|
.load(r#"return math.type(utils.fromJSON("42")) == "integer""#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: f64 = lua.load(r#"return utils.fromJSON("3.14")"#).eval().unwrap(); |
||||||
|
assert!((result - 3.14).abs() < 0.001); |
||||||
|
|
||||||
|
let result: bool = lua |
||||||
|
.load(r#"return math.type(utils.fromJSON("3.14")) == "float""#) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
|
||||||
|
let result: String = lua.load(r#"return utils.fromJSON('"hello"')"#).eval().unwrap(); |
||||||
|
assert_eq!(result, "hello"); |
||||||
|
|
||||||
|
// Array
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local arr = utils.fromJSON("[1, 2, 3]") |
||||||
|
return arr[1] + arr[2] + arr[3] |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 6); |
||||||
|
|
||||||
|
// Object
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local obj = utils.fromJSON('{"a": 10, "b": 20}') |
||||||
|
return obj.a + obj.b |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 30); |
||||||
|
|
||||||
|
// Nested structure
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local data = utils.fromJSON('{"nested": {"value": 42}}') |
||||||
|
return data.nested.value |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 42); |
||||||
|
|
||||||
|
// Invalid JSON should error, and the message names the function
|
||||||
|
let err = lua.load(r#"return utils.fromJSON("not valid json")"#).exec().unwrap_err(); |
||||||
|
assert!(err.to_string().contains("fromJSON"), "got: {err}"); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_json_roundtrip() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Round-trip test: Lua -> JSON -> Lua
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local original = {x = 10, y = 20, items = {1, 2, 3}} |
||||||
|
local json = utils.toJSON(original) |
||||||
|
local restored = utils.fromJSON(json) |
||||||
|
return restored.x + restored.y + restored.items[1] |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 31); // 10 + 20 + 1
|
||||||
|
|
||||||
|
// NULL survives a Lua -> JSON -> Lua round trip
|
||||||
|
let result: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local restored = utils.fromJSON(utils.toJSON({a = utils.NULL})) |
||||||
|
return utils.isNull(restored.a) |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(result); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_try() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Success case: returns value, nil error
|
||||||
|
let (value, has_error): (i64, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local val, err = utils.try(function() return 42 end) |
||||||
|
return val, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(value, 42); |
||||||
|
assert!(!has_error); |
||||||
|
|
||||||
|
// Error case: returns nil value, error
|
||||||
|
let (is_nil, has_error): (bool, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local val, err = utils.try(function() error("boom") end) |
||||||
|
return val == nil, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(is_nil); |
||||||
|
assert!(has_error); |
||||||
|
|
||||||
|
// Error message is preserved
|
||||||
|
let contains_boom: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local val, err = utils.try(function() error("boom") end) |
||||||
|
return tostring(err):find("boom") ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(contains_boom); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_try_shorthand() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// pcall-style shorthand: the function and its arguments, no closure needed
|
||||||
|
let (value, has_error): (String, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local val, err = utils.try(string.rep, "ab", 3) |
||||||
|
return val, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(value, "ababab"); |
||||||
|
assert!(!has_error); |
||||||
|
|
||||||
|
// Errors thrown by the called function are captured the same way as with a closure
|
||||||
|
let captured: bool = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local val, err = utils.try(error, "boom") |
||||||
|
return val == nil and tostring(err):find("boom") ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(captured); |
||||||
|
|
||||||
|
// nil arguments are forwarded with the argument count preserved (pcall semantics)
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local function countArgs(...) return select('#', ...) end |
||||||
|
local n = utils.try(countArgs, nil, nil, nil) |
||||||
|
return n |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 3); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_tryn() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// Success case with multiple return values
|
||||||
|
let (a, b, c, has_error): (i64, i64, i64, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a, b, c, err = utils.tryn(3, function() return 1, 2, 3 end) |
||||||
|
return a, b, c, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!((a, b, c), (1, 2, 3)); |
||||||
|
assert!(!has_error); |
||||||
|
|
||||||
|
// Error case: all values are nil, error is present
|
||||||
|
let (all_nil, has_error): (bool, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a, b, c, err = utils.tryn(3, function() error("fail") end) |
||||||
|
return a == nil and b == nil and c == nil, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(all_nil); |
||||||
|
assert!(has_error); |
||||||
|
|
||||||
|
// Fewer return values than expected: pads with nil
|
||||||
|
let (a, b_nil, c_nil, has_error): (i64, bool, bool, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a, b, c, err = utils.tryn(3, function() return 100 end) |
||||||
|
return a, b == nil, c == nil, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(a, 100); |
||||||
|
assert!(b_nil); |
||||||
|
assert!(c_nil); |
||||||
|
assert!(!has_error); |
||||||
|
|
||||||
|
// More return values than expected: truncates
|
||||||
|
let (a, b, has_error): (i64, i64, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local a, b, err = utils.tryn(2, function() return 1, 2, 3, 4, 5 end) |
||||||
|
return a, b, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!((a, b), (1, 2)); |
||||||
|
assert!(!has_error); |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn test_tryn_shorthand() { |
||||||
|
let lua = super::lua(); |
||||||
|
|
||||||
|
// pcall-style shorthand: arguments after the callback are passed to it
|
||||||
|
let (q, r, has_error): (i64, i64, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local function divmod(a, b) return a // b, a % b end
|
||||||
|
local q, r, err = utils.tryn(2, divmod, 17, 5) |
||||||
|
return q, r, err ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!((q, r), (3, 2)); |
||||||
|
assert!(!has_error); |
||||||
|
|
||||||
|
// Error case: arguments are passed, the error lands in the last slot
|
||||||
|
let (all_nil, captured): (bool, bool) = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local function fail(msg) error("failed: " .. msg) end |
||||||
|
local a, b, err = utils.tryn(2, fail, "badly") |
||||||
|
return a == nil and b == nil, tostring(err):find("failed: badly") ~= nil |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert!(all_nil); |
||||||
|
assert!(captured); |
||||||
|
|
||||||
|
// nil arguments are forwarded with the argument count preserved (pcall semantics)
|
||||||
|
let result: i64 = lua |
||||||
|
.load( |
||||||
|
r#" |
||||||
|
local function countArgs(...) return select('#', ...) end |
||||||
|
local n, err = utils.tryn(1, countArgs, nil, 5, nil) |
||||||
|
return n |
||||||
|
"#, |
||||||
|
) |
||||||
|
.eval() |
||||||
|
.unwrap(); |
||||||
|
assert_eq!(result, 3); |
||||||
|
} |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
use std::borrow::Cow; |
||||||
|
|
||||||
|
use mlua::Value as LuaValue; |
||||||
|
use serde_json::Value as JsonValue; |
||||||
|
|
||||||
|
/// Pretty-print a Lua value for humans (the backend of `utils.dump`).
|
||||||
|
/// Ported from flowbox-rt's fb_lua lua_dump.rs, adapted to mlua 0.11.
|
||||||
|
pub(super) fn dump(value: LuaValue) -> Cow<'static, str> { |
||||||
|
dump_inner(value, DumpPosition::Value, 0) |
||||||
|
} |
||||||
|
|
||||||
|
enum DumpPosition { |
||||||
|
Key, |
||||||
|
Value, |
||||||
|
} |
||||||
|
|
||||||
|
fn keywrap(value: Cow<'static, str>, position: DumpPosition) -> Cow<'static, str> { |
||||||
|
match position { |
||||||
|
DumpPosition::Key => format!("[{value}]").into(), |
||||||
|
DumpPosition::Value => value, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fn dump_inner(value: LuaValue, position: DumpPosition, recursion_depth: usize) -> Cow<'static, str> { |
||||||
|
let res: Cow<'static, str> = match value { |
||||||
|
LuaValue::Nil => "nil".into(), |
||||||
|
LuaValue::Boolean(v) => if v { "true" } else { "false" }.into(), |
||||||
|
LuaValue::Integer(v) => v.to_string().into(), |
||||||
|
LuaValue::Number(v) => { |
||||||
|
if v.fract() == 0.0 { |
||||||
|
// ensure decimal places are shown to indicate it is a float
|
||||||
|
format!("{:.1}", v.trunc()).into() |
||||||
|
} else { |
||||||
|
v.to_string().into() |
||||||
|
} |
||||||
|
} |
||||||
|
LuaValue::String(v) => { |
||||||
|
let stringified = v.to_string_lossy().to_string(); |
||||||
|
match position { |
||||||
|
DumpPosition::Key => { |
||||||
|
// return to bypass the keywrap
|
||||||
|
return if stringified.char_indices().any(|(index, c)| { |
||||||
|
if index == 0 { |
||||||
|
!c.is_ascii_alphabetic() && c != '_' |
||||||
|
} else { |
||||||
|
!c.is_ascii_alphanumeric() && c != '_' |
||||||
|
} |
||||||
|
}) { |
||||||
|
// json-serialize the string to add quotes and escapes
|
||||||
|
format!("[{}]", serde_json::to_string(&JsonValue::String(stringified)).unwrap()).into() |
||||||
|
} else { |
||||||
|
// string lua key can be used as is {foo="bar"}
|
||||||
|
stringified.into() |
||||||
|
}; |
||||||
|
} |
||||||
|
DumpPosition::Value => serde_json::to_string(&JsonValue::String(stringified)).unwrap().into(), |
||||||
|
} |
||||||
|
} |
||||||
|
LuaValue::Table(t) => { |
||||||
|
if recursion_depth > 20 { |
||||||
|
// Flow through keywrap so a table used as a key still renders as
|
||||||
|
// `[<TRUNCATED>]`, consistent with every other key.
|
||||||
|
return keywrap("<TRUNCATED>".into(), position); |
||||||
|
} |
||||||
|
let recursion_depth = recursion_depth + 1; |
||||||
|
|
||||||
|
let mut buf = "{".to_string(); |
||||||
|
|
||||||
|
let mut first = true; |
||||||
|
let mut list_next = Some(1); |
||||||
|
for pair in t.pairs::<LuaValue, LuaValue>() { |
||||||
|
let Ok((k, v)) = pair else { |
||||||
|
list_next = None; |
||||||
|
continue; |
||||||
|
}; |
||||||
|
|
||||||
|
if !first { |
||||||
|
buf.push_str(", "); |
||||||
|
} |
||||||
|
first = false; |
||||||
|
|
||||||
|
// special case for sequential numeric keys, starting at 1
|
||||||
|
if let Some(list_index) = list_next { |
||||||
|
if let LuaValue::Integer(index) = k |
||||||
|
&& index == list_index |
||||||
|
{ |
||||||
|
buf.push_str(&dump_inner(v, DumpPosition::Value, recursion_depth)); |
||||||
|
list_next = Some(list_index + 1); |
||||||
|
continue; |
||||||
|
} |
||||||
|
list_next = None; |
||||||
|
} |
||||||
|
|
||||||
|
buf.push_str(&format!( |
||||||
|
"{}={}", |
||||||
|
dump_inner(k, DumpPosition::Key, recursion_depth), |
||||||
|
dump_inner(v, DumpPosition::Value, recursion_depth) |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
buf.push('}'); |
||||||
|
buf.into() |
||||||
|
} |
||||||
|
// The null pointer lightuserdata is the JSON null sentinel (utils.NULL)
|
||||||
|
LuaValue::LightUserData(ud) if ud.0.is_null() => "NULL".into(), |
||||||
|
// Complex objects can't be displayed
|
||||||
|
LuaValue::LightUserData(_) => "<LightUserData>".into(), |
||||||
|
LuaValue::Function(_) => "<Function>".into(), |
||||||
|
LuaValue::Thread(_) => "<Thread>".into(), |
||||||
|
LuaValue::UserData(_) => "<UserData>".into(), |
||||||
|
LuaValue::Error(e) => format!("<Error:{e}>").into(), |
||||||
|
other => format!("<{}>", other.type_name()).into(), |
||||||
|
}; |
||||||
|
|
||||||
|
keywrap(res, position) |
||||||
|
} |
||||||
Loading…
Reference in new issue