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