mumu-test 0.1.2

Test suite plugin for the Lava language
Documentation
use crate::suite::mark_fail;
use mumu::parser::types::Value;
use mumu::parser::interpreter::Interpreter;

pub fn expect_equal_bridge_fn(_interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!("expect_equal => expected 2 args, got {}", args.len()));
    }
    let actual = args.remove(0);
    let expected = args.remove(0);

    if let Err(msg) = values_equal(&actual, &expected) {
        mark_fail(&format!("expect_equal fail: actual={:?}, expected={:?}\n{}", actual, expected, msg));
    }
    Ok(Value::Bool(true))
}

pub fn expect_not_equal_bridge_fn(_interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!("expect_not_equal => expected 2 args, got {}", args.len()));
    }
    let actual = args.remove(0);
    let expected = args.remove(0);

    if values_equal(&actual, &expected).is_ok() {
        mark_fail(&format!("expect_not_equal fail: both are {:?}", actual));
    }
    Ok(Value::Bool(true))
}

pub fn has_key_bridge_fn(_interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 2 {
        return Err(format!("has_key => expected 2 args, got {}", args.len()));
    }
    let subject = args.remove(0);
    let key_val = args.remove(0);

    let map = match subject {
        Value::KeyedArray(m) => m,
        other => {
            mark_fail(&format!("has_key => subject not keyed array, got {:?}", other));
            return Ok(Value::Bool(true));
        }
    };
    let key_str = match key_val {
        Value::SingleString(s) => s,
        Value::StrArray(ss) if ss.len() == 1 => ss[0].clone(),
        other => {
            mark_fail(&format!("has_key => key must be single string, got {:?}", other));
            return Ok(Value::Bool(true));
        }
    };

    if !map.contains_key(&key_str) {
        mark_fail(&format!("has_key => missing key '{}'", key_str));
    }
    Ok(Value::Bool(true))
}

pub fn prop_equals_bridge_fn(_interp: &mut Interpreter, mut args: Vec<Value>) -> Result<Value, String> {
    if args.len() != 3 {
        return Err(format!("prop_equals => expected 3 args, got {}", args.len()));
    }
    let subject = args.remove(0);
    let key_val = args.remove(0);
    let expected_val = args.remove(0);

    let map = match subject {
        Value::KeyedArray(m) => m,
        other => {
            mark_fail(&format!("prop_equals => subject is not keyed, got {:?}", other));
            return Ok(Value::Bool(true));
        }
    };
    let key_str = match key_val {
        Value::SingleString(s) => s,
        Value::StrArray(sa) if sa.len() == 1 => sa[0].clone(),
        other => {
            mark_fail(&format!("prop_equals => key must be single string, got {:?}", other));
            return Ok(Value::Bool(true));
        }
    };

    match map.get(&key_str) {
        Some(actual_val) => {
            if values_equal(actual_val, &expected_val).is_err() {
                mark_fail(&format!(
                    "prop_equals => mismatch for '{}': got={:?}, expected={:?}",
                    key_str, actual_val, expected_val
                ));
            }
        }
        None => {
            mark_fail(&format!("prop_equals => missing key '{}'", key_str));
        }
    }

    Ok(Value::Bool(true))
}

// Recursive, float/array/deep tolerant
pub fn values_equal(a: &Value, b: &Value) -> Result<(), String> {
    match (a, b) {
        (Value::Int(x), Value::Int(y)) => if x == y { Ok(()) } else { Err(format!("Int {} != {}", x, y)) },
        (Value::Long(x), Value::Long(y)) => if x == y { Ok(()) } else { Err(format!("Long {} != {}", x, y)) },
        (Value::Bool(x), Value::Bool(y)) => if x == y { Ok(()) } else { Err(format!("Bool {} != {}", x, y)) },
        (Value::SingleString(x), Value::SingleString(y)) => if x == y { Ok(()) } else { Err(format!("String {:?} != {:?}", x, y)) },
        (Value::Float(x), Value::Float(y)) => {
            if (x - y).abs() <= 1e-9 || (x.is_nan() && y.is_nan()) {
                Ok(())
            } else {
                Err(format!("Float {} != {} (eps=1e-9)", x, y))
            }
        }
        (Value::IntArray(xs), Value::IntArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("IntArray length {} != {}", xs.len(), ys.len()));
            }
            for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
                if x != y {
                    return Err(format!("IntArray[{}] {} != {}", i, x, y));
                }
            }
            Ok(())
        }
        (Value::FloatArray(xs), Value::FloatArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("FloatArray length {} != {}", xs.len(), ys.len()));
            }
            for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
                if (x - y).abs() > 1e-9 && !(x.is_nan() && y.is_nan()) {
                    return Err(format!("FloatArray[{}] {} != {} (eps=1e-9)", i, x, y));
                }
            }
            Ok(())
        }
        (Value::StrArray(xs), Value::StrArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("StrArray length {} != {}", xs.len(), ys.len()));
            }
            for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
                if x != y {
                    return Err(format!("StrArray[{}] {:?} != {:?}", i, x, y));
                }
            }
            Ok(())
        }
        (Value::BoolArray(xs), Value::BoolArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("BoolArray length {} != {}", xs.len(), ys.len()));
            }
            for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
                if x != y {
                    return Err(format!("BoolArray[{}] {} != {}", i, x, y));
                }
            }
            Ok(())
        }
        (Value::MixedArray(xs), Value::MixedArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("MixedArray length {} != {}", xs.len(), ys.len()));
            }
            for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
                if let Err(e) = values_equal(x, y) {
                    return Err(format!("MixedArray[{}] failed: {}", i, e));
                }
            }
            Ok(())
        }
        (Value::KeyedArray(mx), Value::KeyedArray(my)) => {
            if mx.len() != my.len() {
                return Err(format!("KeyedArray len {} != {}", mx.len(), my.len()));
            }
            for (k, vx) in mx {
                if let Some(vy) = my.get(k) {
                    if let Err(e) = values_equal(vx, vy) {
                        return Err(format!("KeyedArray[{:?}] failed: {}", k, e));
                    }
                } else {
                    return Err(format!("KeyedArray missing key {:?}", k));
                }
            }
            Ok(())
        }
        (Value::Int2DArray(xs), Value::Int2DArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("Int2DArray outer len {} != {}", xs.len(), ys.len()));
            }
            for (i, (r1, r2)) in xs.iter().zip(ys).enumerate() {
                if r1.len() != r2.len() {
                    return Err(format!("Int2DArray row {} len {} != {}", i, r1.len(), r2.len()));
                }
                for (j, (x, y)) in r1.iter().zip(r2).enumerate() {
                    if x != y {
                        return Err(format!("Int2DArray[{}][{}] {} != {}", i, j, x, y));
                    }
                }
            }
            Ok(())
        }
        (Value::Float2DArray(xs), Value::Float2DArray(ys)) => {
            if xs.len() != ys.len() {
                return Err(format!("Float2DArray outer len {} != {}", xs.len(), ys.len()));
            }
            for (i, (r1, r2)) in xs.iter().zip(ys).enumerate() {
                if r1.len() != r2.len() {
                    return Err(format!("Float2DArray row {} len {} != {}", i, r1.len(), r2.len()));
                }
                for (j, (x, y)) in r1.iter().zip(r2).enumerate() {
                    if (x - y).abs() > 1e-9 && !(x.is_nan() && y.is_nan()) {
                        return Err(format!("Float2DArray[{}][{}] {} != {}", i, j, x, y));
                    }
                }
            }
            Ok(())
        }
        _ => if a == b { Ok(()) } else { Err(format!("actual={:?}, expected={:?} (no deep match)", a, b)) },
    }
}