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