Skip to main content

mlua_batteries/
time.rs

1//! Time and measurement module.
2//!
3//! ```lua
4//! local time = std.time
5//! local ts = time.now()       -- epoch seconds (f64)
6//! local ms = time.millis()    -- epoch milliseconds (i64)
7//! time.sleep(0.5)             -- block for 0.5 seconds
8//! local elapsed, result = time.measure(function() return "done" end)
9//! ```
10
11use mlua::prelude::*;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14use crate::util::with_config;
15
16pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
17    let t = lua.create_table()?;
18
19    t.set(
20        "now",
21        lua.create_function(|_, _: ()| {
22            let dur = SystemTime::now()
23                .duration_since(UNIX_EPOCH)
24                .map_err(LuaError::external)?;
25            Ok(dur.as_secs_f64())
26        })?,
27    )?;
28
29    t.set(
30        "millis",
31        lua.create_function(|_, _: ()| {
32            let dur = SystemTime::now()
33                .duration_since(UNIX_EPOCH)
34                .map_err(LuaError::external)?;
35            // as_millis() returns u128. Saturate to i64::MAX (~292 million years).
36            let ms: i64 = dur.as_millis().try_into().unwrap_or(i64::MAX);
37            Ok(ms)
38        })?,
39    )?;
40
41    t.set(
42        "sleep",
43        lua.create_function(|lua, seconds: f64| {
44            if !seconds.is_finite() || seconds < 0.0 {
45                return Err(LuaError::external(format!(
46                    "sleep duration must be a finite non-negative number, got {seconds}"
47                )));
48            }
49            let max_secs = with_config(lua, |c| c.max_sleep_secs)?;
50            if seconds > max_secs {
51                return Err(LuaError::external(format!(
52                    "sleep duration must not exceed {max_secs} seconds"
53                )));
54            }
55            std::thread::sleep(Duration::from_secs_f64(seconds));
56            Ok(())
57        })?,
58    )?;
59
60    t.set(
61        "measure",
62        lua.create_function(|_, func: LuaFunction| {
63            let start = std::time::Instant::now();
64            let result: LuaMultiValue = func.call(())?;
65            let elapsed = start.elapsed().as_secs_f64();
66            let mut ret = vec![LuaValue::Number(elapsed)];
67            ret.extend(result);
68            Ok(LuaMultiValue::from_vec(ret))
69        })?,
70    )?;
71
72    Ok(t)
73}
74
75#[cfg(test)]
76mod tests {
77    use mlua::Lua;
78
79    use crate::util::test_eval as eval;
80
81    #[test]
82    fn now_returns_positive() {
83        let ts: f64 = eval("return std.time.now()");
84        assert!(ts > 1_000_000_000.0);
85    }
86
87    #[test]
88    fn millis_returns_positive() {
89        let ms: i64 = eval("return std.time.millis()");
90        assert!(ms > 1_000_000_000_000);
91    }
92
93    #[test]
94    fn now_and_millis_consistent() {
95        let diff: f64 = eval(
96            r#"
97            local sec = std.time.now()
98            local ms = std.time.millis()
99            return math.abs(sec * 1000 - ms)
100        "#,
101        );
102        assert!(diff < 100.0);
103    }
104
105    #[test]
106    fn sleep_pauses() {
107        let elapsed: f64 = eval(
108            r#"
109            local before = std.time.now()
110            std.time.sleep(0.05)
111            return std.time.now() - before
112        "#,
113        );
114        assert!(elapsed >= 0.04);
115    }
116
117    #[test]
118    fn sleep_zero_is_valid() {
119        let ok: bool = eval(
120            r#"
121            std.time.sleep(0)
122            return true
123        "#,
124        );
125        assert!(ok);
126    }
127
128    #[test]
129    fn sleep_negative_returns_error() {
130        let lua = Lua::new();
131        crate::register_all(&lua, "std").unwrap();
132        let result: mlua::Result<mlua::Value> = lua.load("std.time.sleep(-1)").eval();
133        assert!(result.is_err());
134    }
135
136    #[test]
137    fn sleep_nan_returns_error() {
138        let lua = Lua::new();
139        crate::register_all(&lua, "std").unwrap();
140        let result: mlua::Result<mlua::Value> = lua.load("std.time.sleep(0/0)").eval();
141        assert!(result.is_err());
142    }
143
144    #[test]
145    fn sleep_exceeding_max_returns_error() {
146        let lua = Lua::new();
147        crate::register_all(&lua, "std").unwrap();
148        let result: mlua::Result<mlua::Value> = lua.load("std.time.sleep(86401)").eval();
149        assert!(result.is_err());
150    }
151
152    #[test]
153    fn custom_max_sleep_enforced() {
154        let lua = Lua::new();
155        let config = crate::config::Config::builder()
156            .max_sleep_secs(1.0)
157            .build()
158            .unwrap();
159        crate::register_all_with(&lua, "std", config).unwrap();
160
161        let result: mlua::Result<mlua::Value> = lua.load("std.time.sleep(2)").eval();
162        assert!(result.is_err());
163    }
164
165    #[test]
166    fn measure_returns_elapsed_and_result() {
167        let lua = Lua::new();
168        crate::register_all(&lua, "std").unwrap();
169        let (elapsed, value): (f64, String) = lua
170            .load(
171                r#"
172                local elapsed, result = std.time.measure(function()
173                    std.time.sleep(0.05)
174                    return "done"
175                end)
176                return elapsed, result
177            "#,
178            )
179            .eval()
180            .unwrap();
181        assert!(elapsed >= 0.04);
182        assert_eq!(value, "done");
183    }
184
185    #[test]
186    fn measure_propagates_error() {
187        let lua = Lua::new();
188        crate::register_all(&lua, "std").unwrap();
189        let result: mlua::Result<mlua::Value> = lua
190            .load(r#"return std.time.measure(function() error("boom") end)"#)
191            .eval();
192        assert!(result.is_err());
193    }
194}