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.
313 lines
9.1 KiB
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);
|
|
}
|
|
|