Skip to main content

mlua_lspec/
framework.rs

1use mlua::prelude::*;
2
3use crate::types::{TestResult, TestSummary};
4
5const LUST_LUA: &str = include_str!("../lua/lust.lua");
6
7/// Register the lust test framework into the given Lua VM.
8///
9/// After this call, `lust` is available as a global table with
10/// `describe`, `it`, `expect`, `before`, `after`, `spy`, and
11/// `get_results`.
12pub fn register(lua: &Lua) -> LuaResult<()> {
13    let lust: LuaTable = lua.load(LUST_LUA).set_name("lust.lua").eval()?;
14    lua.globals().set("lust", lust)?;
15    Ok(())
16}
17
18/// Collect structured test results from the lust framework.
19///
20/// Call this after all `describe`/`it` blocks have executed.
21pub fn collect_results(lua: &Lua) -> LuaResult<TestSummary> {
22    let lust: LuaTable = lua.globals().get("lust")?;
23    let get_results: LuaFunction = lust.get("get_results")?;
24    let results: LuaTable = get_results.call(())?;
25
26    let passed: usize = results.get("passed")?;
27    let failed: usize = results.get("failed")?;
28    let total: usize = results.get("total")?;
29
30    let tests_table: LuaTable = results.get("tests")?;
31    let mut tests = Vec::with_capacity(total);
32
33    for pair in tests_table.pairs::<usize, LuaTable>() {
34        let (_, t) = pair?;
35        tests.push(TestResult {
36            suite: t.get::<String>("suite")?,
37            name: t.get::<String>("name")?,
38            passed: t.get::<bool>("passed")?,
39            error: t.get::<Option<String>>("error")?,
40        });
41    }
42
43    Ok(TestSummary {
44        passed,
45        failed,
46        total,
47        tests,
48    })
49}
50
51/// Run Lua test code with the lust framework pre-loaded.
52///
53/// Creates a fresh Lua VM, registers lust and test doubles,
54/// executes `code`, and returns the structured test summary.
55///
56/// Lua's `print` is replaced with a no-op to prevent stdout
57/// pollution.  Callers who need console output should use
58/// [`register`] on their own `Lua` instance where `print`
59/// remains intact.
60pub fn run_tests(code: &str, chunk_name: &str) -> Result<TestSummary, String> {
61    let lua = Lua::new();
62
63    register(&lua).map_err(|e| format!("Failed to register test framework: {e}"))?;
64    crate::doubles::register(&lua).map_err(|e| format!("Failed to register test doubles: {e}"))?;
65
66    // Suppress lust's print() output so that callers using stdio
67    // transports (e.g. MCP servers) are not polluted.
68    lua.globals()
69        .set(
70            "print",
71            lua.create_function(|_, _: mlua::Variadic<LuaValue>| Ok(()))
72                .map_err(|e| format!("Failed to override print: {e}"))?,
73        )
74        .map_err(|e| format!("Failed to override print: {e}"))?;
75
76    lua.load(code)
77        .set_name(chunk_name)
78        .exec()
79        .map_err(|e| format!("Test execution error: {e}"))?;
80
81    collect_results(&lua).map_err(|e| format!("Failed to collect results: {e}"))
82}