test-mumu 0.1.10

Test suite plugin for the Lava language
Documentation
// src/describe_it.rs
use crate::suite::{TestEntry, FileReport, GLOBAL_SUITE, CURRENT_TEST_ARC, mark_fail};
use mumu::{
    parser::interpreter::Interpreter,
    parser::types::{FunctionValue, Value},
};
use serde_json::to_string;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{thread, time::Duration};

/// Invoke **any** function value:
/// - If it's a dynamic Named function we dispatch through the dynamic registry.
/// - Otherwise we **fallback** to the interpreter's general apply() path,
///   which correctly handles inline/user-defined functions (even if they are
///   compiled to synthetic named callables).
fn call_any_function(
    interp: &mut Interpreter,
    func: Box<FunctionValue>,
    args: Vec<Value>,
) -> Result<Value, String> {
    match *func {
        FunctionValue::Named(ref name) => {
            if let Some(df) = interp.get_dynamic_function(name) {
                (df.lock().unwrap())(interp, args)
            } else {
                // Fallback: user-defined (non-dynamic) function
                mumu::parser::interpreter::apply::apply_function_value(
                    interp,
                    Box::new(FunctionValue::Named(name.clone())),
                    args,
                )
            }
        }
        other => {
            mumu::parser::interpreter::apply::apply_function_value(interp, Box::new(other), args)
        }
    }
}

pub fn describe_bridge_fn(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!("describe => expected 2 args, got {}", args.len()));
    }
    let suite_name = match args.remove(0) {
        Value::SingleString(s) => s,
        Value::StrArray(a) if a.len() == 1 => a[0].clone(),
        other => return Err(format!("describe => first arg must be single string, got {:?}", other)),
    };

    // Reset per-call global state (one JSON line per describe(..))
    {
        let mut gs = GLOBAL_SUITE.lock().unwrap();
        gs.suite_name = Some(suite_name.clone());
        gs.tests.clear();
    }

    let fn_val = match args.remove(0) {
        Value::Function(fb) => fb,
        other => return Err(format!("describe => second arg must be function, got {:?}", other)),
    };

    // Run suite body
    if interp.is_verbose() {
        eprintln!("[describe] => invoking suite body for {:?}", suite_name);
    }
    match call_any_function(interp, fn_val.clone(), vec![]) {
        Ok(ret) => {
            // Optional continuation support (if the callback returns a function)
            if let Value::Function(f2) = ret {
                if interp.is_verbose() {
                    eprintln!("[describe] => invoking suite continuation for {:?}", suite_name);
                }
                if let Err(e) = call_any_function(interp, f2, vec![]) {
                    mark_fail(&format!("describe continuation error: {e}"));
                }
            }
        }
        Err(e) => {
            // Suite body failed — record a synthetic failure so it’s visible
            mark_fail(&format!("describe body error: {e}"));
        }
    }

    // Best-effort: give any task-pollers a chance to drain (harmless if none exist)
    loop {
        let mut total_tasks = 0;
        for poller_name in &["net:check_tasks", "sys:check_tasks", "process:check_tasks"] {
            if let Some(dfn) = interp.get_dynamic_function(poller_name) {
                if let Ok(val) = dfn.lock().unwrap()(interp, vec![]) {
                    if let Value::Int(n) = val {
                        total_tasks += n;
                    }
                }
            }
        }
        if total_tasks == 0 {
            break;
        }
        thread::sleep(Duration::from_millis(30));
    }

    // Emit JSON line with this suite’s report
    let report = {
        let gs = GLOBAL_SUITE.lock().unwrap();
        let mut out_tests = Vec::new();
        for arc_test in &gs.tests {
            let t = arc_test.lock().unwrap();
            out_tests.push(t.clone());
        }
        FileReport {
            suite: gs.suite_name.clone().unwrap_or_default(),
            tests: out_tests,
        }
    };

    let js = to_string(&report).map_err(|e| format!("describe => JSON err: {}", e))?;
    println!("{}", js);

    Ok(Value::Bool(true))
}

pub fn it_bridge_fn(interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!("it => expected 2 args, got {}", args.len()));
    }
    let test_name = match args.remove(0) {
        Value::SingleString(s) => s,
        Value::StrArray(a) if a.len() == 1 => a[0].clone(),
        other => return Err(format!("it => first arg must be single string, got {:?}", other)),
    };

    let fn_val = match args.remove(0) {
        Value::Function(fb) => fb,
        other => return Err(format!("it => second arg must be function, got {:?}", other)),
    };

    let start = current_time_micro();
    let new_test = TestEntry {
        name: test_name.clone(),
        passed: true,
        time_us: 0,
        output: String::new(),
    };

    let arc_test = Arc::new(Mutex::new(new_test));
    CURRENT_TEST_ARC.with(|cell| {
        *cell.borrow_mut() = Some(arc_test.clone());
    });

    // Run test body (and optional continuation) with error awareness.
    if interp.is_verbose() {
        eprintln!("[it] => running test {:?}", test_name);
    }
    match call_any_function(interp, fn_val.clone(), vec![]) {
        Ok(ret) => {
            if let Value::Function(f2) = ret {
                if let Err(e) = call_any_function(interp, f2, vec![]) {
                    mark_fail(&format!("it continuation error: {e}"));
                }
            }
        }
        Err(e) => {
            mark_fail(&format!("it body error: {e}"));
        }
    }

    let dur = current_time_micro() - start;
    {
        let mut lock_test = arc_test.lock().unwrap();
        lock_test.time_us = dur;
    }
    {
        let mut gs = GLOBAL_SUITE.lock().unwrap();
        gs.tests.push(arc_test);
    }

    Ok(Value::Bool(true))
}

fn current_time_micro() -> i64 {
    let now = SystemTime::now();
    now.duration_since(UNIX_EPOCH).unwrap().as_micros() as i64
}