Lua runner with rich builtin stdlib ("lunascript")
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
luna/src/lua_tests/async_tests.rs

313 lines
9.1 KiB

//! 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);
}