#[derive(Debug, Clone)]
pub struct LocationContext {
pub file: Option<std::path::PathBuf>,
pub macro_stack: Vec<String>,
pub include_stack: Vec<std::path::PathBuf>,
}
pub mod interpreter;
pub mod lexer;
pub mod scope;
#[cfg(feature = "yaml")]
pub mod yaml_tag_handler;
pub(crate) use scope::*;
pub use interpreter::EvalError;
#[cfg(test)]
mod property_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_scope_basic_shadowing() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
let result1 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result1, "10", "Global x should be 10");
let mut scope = HashMap::new();
scope.insert("x".to_string(), "5".to_string());
processor.push_scope(scope);
let result2 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result2, "5", "Scoped x should be 5 (shadowing global)");
processor.pop_scope();
let result3 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result3, "10", "After pop, x should be 10 again");
}
#[test]
fn test_scope_fallback_to_global() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
processor.add_raw_property("y".to_string(), "20".to_string());
let mut scope = HashMap::new();
scope.insert("x".to_string(), "5".to_string());
processor.push_scope(scope);
let result_x = processor.substitute_text("${x}", None).unwrap();
let result_y = processor.substitute_text("${y}", None).unwrap();
assert_eq!(result_x, "5", "Scoped x should be 5");
assert_eq!(result_y, "20", "y should fall back to global value 20");
processor.pop_scope();
}
#[test]
fn test_scope_nested_shadowing() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
let mut scope1 = HashMap::new();
scope1.insert("x".to_string(), "20".to_string());
processor.push_scope(scope1);
let result1 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result1, "20", "First scope: x should be 20");
let mut scope2 = HashMap::new();
scope2.insert("x".to_string(), "30".to_string());
processor.push_scope(scope2);
let result2 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result2, "30", "Nested scope: x should be 30");
processor.pop_scope();
let result3 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result3, "20", "After pop, x should be 20 again");
processor.pop_scope();
let result4 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result4, "10", "After second pop, x should be global 10");
}
#[test]
fn test_scope_cache_bypass() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
let result1 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(result1, "10");
let mut scope = HashMap::new();
scope.insert("x".to_string(), "5".to_string());
processor.push_scope(scope);
let result2 = processor.substitute_text("${x}", None).unwrap();
assert_eq!(
result2, "5",
"Scoped value should bypass cache and return 5, not cached 10"
);
processor.pop_scope();
}
#[test]
fn test_scope_with_expressions() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("base".to_string(), "10".to_string());
let mut scope = HashMap::new();
scope.insert("multiplier".to_string(), "3".to_string());
processor.push_scope(scope);
let result = processor
.substitute_text("${base * multiplier}", None)
.unwrap();
assert_eq!(result, "30", "Should compute 10 * 3 = 30");
processor.pop_scope();
}
#[test]
fn test_scope_undefined_property() {
let processor: EvalContext = EvalContext::new();
let mut scope = HashMap::new();
scope.insert("x".to_string(), "5".to_string());
processor.push_scope(scope);
let result = processor.substitute_text("${y}", None);
assert!(
result.is_err(),
"Undefined property should error even in scope"
);
processor.pop_scope();
}
#[test]
fn test_substitute_text_simple_arithmetic() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
processor.add_raw_property("y".to_string(), "5".to_string());
let add = processor.substitute_text("${x + y}", None).unwrap();
assert_eq!(add, "15");
let multiply = processor.substitute_text("${x * y}", None).unwrap();
assert_eq!(multiply, "50");
}
#[test]
fn test_substitute_text_with_functions() {
let processor: EvalContext = EvalContext::new();
let result = processor.substitute_text("${abs(-5)}", None).unwrap();
assert_eq!(result, "5");
let result2 = processor.substitute_text("${max(10, 20)}", None).unwrap();
assert_eq!(result2, "20");
}
#[test]
fn test_substitute_text_multiple_variables() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("prefix".to_string(), "robot_".to_string());
processor.add_raw_property("name".to_string(), "arm".to_string());
let result = processor.substitute_text("${prefix}${name}", None).unwrap();
assert_eq!(result, "robot_arm");
}
#[test]
fn test_eval_boolean_literals() {
let processor: EvalContext = EvalContext::new();
assert_eq!(processor.eval_boolean("True", None).unwrap(), true);
assert_eq!(processor.eval_boolean("False", None).unwrap(), false);
assert_eq!(processor.eval_boolean("1", None).unwrap(), true);
assert_eq!(processor.eval_boolean("0", None).unwrap(), false);
}
#[test]
fn test_eval_boolean_comparisons() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
processor.add_raw_property("y".to_string(), "20".to_string());
assert_eq!(processor.eval_boolean("${x < y}", None).unwrap(), true);
assert_eq!(processor.eval_boolean("${x > y}", None).unwrap(), false);
assert_eq!(processor.eval_boolean("${x == 10}", None).unwrap(), true);
}
#[test]
fn test_has_property_basic() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("x".to_string(), "10".to_string());
assert_eq!(processor.has_property("x"), true);
assert_eq!(processor.has_property("y"), false);
}
#[test]
fn test_has_property_with_scope() {
let processor: EvalContext = EvalContext::new();
processor.add_raw_property("global".to_string(), "1".to_string());
let mut scope = HashMap::new();
scope.insert("local".to_string(), "2".to_string());
processor.push_scope(scope);
assert_eq!(processor.has_property("global"), true);
assert_eq!(processor.has_property("local"), true);
processor.pop_scope();
assert_eq!(processor.has_property("global"), true);
assert_eq!(processor.has_property("local"), false);
}
#[test]
fn test_substitute_literal_zero() {
let processor: EvalContext = EvalContext::new();
let result = processor.substitute_text("${0}", None).unwrap();
assert_eq!(result, "0", "Should evaluate literal 0 expression");
}
}