use rhai::Engine; use std::sync::{Arc, Mutex}; // ============================================================================= // Test Utilities // ============================================================================= /// Create a Rhai engine with BASIC-like functions registered fn create_basic_engine() -> Engine { let mut engine = Engine::new(); // Register string functions engine.register_fn("INSTR", |haystack: &str, needle: &str| -> i64 { if haystack.is_empty() || needle.is_empty() { return 0; } match haystack.find(needle) { Some(pos) => (pos + 1) as i64, None => 0, } }); engine.register_fn("UPPER", |s: &str| -> String { s.to_uppercase() }); engine.register_fn("UCASE", |s: &str| -> String { s.to_uppercase() }); engine.register_fn("LOWER", |s: &str| -> String { s.to_lowercase() }); engine.register_fn("LCASE", |s: &str| -> String { s.to_lowercase() }); engine.register_fn("LEN", |s: &str| -> i64 { s.len() as i64 }); engine.register_fn("TRIM", |s: &str| -> String { s.trim().to_string() }); engine.register_fn("LTRIM", |s: &str| -> String { s.trim_start().to_string() }); engine.register_fn("RTRIM", |s: &str| -> String { s.trim_end().to_string() }); engine.register_fn("LEFT", |s: &str, count: i64| -> String { let count = count.max(0) as usize; s.chars().take(count).collect() }); engine.register_fn("RIGHT", |s: &str, count: i64| -> String { let count = count.max(0) as usize; let len = s.chars().count(); if count >= len { s.to_string() } else { s.chars().skip(len - count).collect() } }); engine.register_fn("MID", |s: &str, start: i64, length: i64| -> String { let start_idx = if start < 1 { 0 } else { (start - 1) as usize }; let len = length.max(0) as usize; s.chars().skip(start_idx).take(len).collect() }); engine.register_fn("REPLACE", |s: &str, find: &str, replace: &str| -> String { s.replace(find, replace) }); // Register math functions engine.register_fn("ABS", |n: i64| -> i64 { n.abs() }); engine.register_fn("ABS", |n: f64| -> f64 { n.abs() }); engine.register_fn("ROUND", |n: f64| -> i64 { n.round() as i64 }); engine.register_fn("INT", |n: f64| -> i64 { n.trunc() as i64 }); engine.register_fn("FIX", |n: f64| -> i64 { n.trunc() as i64 }); engine.register_fn("FLOOR", |n: f64| -> i64 { n.floor() as i64 }); engine.register_fn("CEIL", |n: f64| -> i64 { n.ceil() as i64 }); engine.register_fn("MAX", |a: i64, b: i64| -> i64 { a.max(b) }); engine.register_fn("MIN", |a: i64, b: i64| -> i64 { a.min(b) }); engine.register_fn("MOD", |a: i64, b: i64| -> i64 { a % b }); engine.register_fn("SGN", |n: i64| -> i64 { n.signum() }); engine.register_fn("SQRT", |n: f64| -> f64 { n.sqrt() }); engine.register_fn("SQR", |n: f64| -> f64 { n.sqrt() }); engine.register_fn("POW", |base: f64, exp: f64| -> f64 { base.powf(exp) }); engine.register_fn("LOG", |n: f64| -> f64 { n.ln() }); engine.register_fn("LOG10", |n: f64| -> f64 { n.log10() }); engine.register_fn("EXP", |n: f64| -> f64 { n.exp() }); engine.register_fn("SIN", |n: f64| -> f64 { n.sin() }); engine.register_fn("COS", |n: f64| -> f64 { n.cos() }); engine.register_fn("TAN", |n: f64| -> f64 { n.tan() }); engine.register_fn("PI", || -> f64 { std::f64::consts::PI }); // Register type conversion engine.register_fn("VAL", |s: &str| -> f64 { s.trim().parse::().unwrap_or(0.0) }); engine.register_fn("STR", |n: i64| -> String { n.to_string() }); engine.register_fn("STR", |n: f64| -> String { n.to_string() }); // Register type checking engine.register_fn("IS_NUMERIC", |value: &str| -> bool { let trimmed = value.trim(); if trimmed.is_empty() { return false; } trimmed.parse::().is_ok() || trimmed.parse::().is_ok() }); engine } /// Mock output collector for TALK commands #[derive(Clone, Default)] struct OutputCollector { messages: Arc>>, } impl OutputCollector { fn new() -> Self { Self { messages: Arc::new(Mutex::new(Vec::new())), } } fn add_message(&self, msg: String) { let mut messages = self.messages.lock().unwrap(); messages.push(msg); } fn get_messages(&self) -> Vec { self.messages.lock().unwrap().clone() } } /// Mock input provider for HEAR commands #[derive(Clone)] struct InputProvider { inputs: Arc>>, index: Arc>, } impl InputProvider { fn new(inputs: Vec) -> Self { Self { inputs: Arc::new(Mutex::new(inputs)), index: Arc::new(Mutex::new(0)), } } fn next_input(&self) -> String { let inputs = self.inputs.lock().unwrap(); let mut index = self.index.lock().unwrap(); if *index < inputs.len() { let input = inputs[*index].clone(); *index += 1; input } else { String::new() } } } /// Create an engine with TALK/HEAR simulation fn create_conversation_engine(output: OutputCollector, input: InputProvider) -> Engine { let mut engine = create_basic_engine(); // Register TALK function let output_clone = output.clone(); engine.register_fn("TALK", move |msg: &str| { output_clone.add_message(msg.to_string()); }); // Register HEAR function engine.register_fn("HEAR", move || -> String { input.next_input() }); engine } // ============================================================================= // String Function Tests with Engine // ============================================================================= #[test] fn test_string_concatenation_in_engine() { let engine = create_basic_engine(); let result: String = engine .eval(r#"let a = "Hello"; let b = " World"; a + b"#) .unwrap(); assert_eq!(result, "Hello World"); } #[test] fn test_string_functions_chain() { let engine = create_basic_engine(); // Test chained operations: UPPER(TRIM(" hello ")) let result: String = engine.eval(r#"UPPER(TRIM(" hello "))"#).unwrap(); assert_eq!(result, "HELLO"); // Test LEN(TRIM(" test ")) let result: i64 = engine.eval(r#"LEN(TRIM(" test "))"#).unwrap(); assert_eq!(result, 4); } #[test] fn test_substring_extraction() { let engine = create_basic_engine(); // LEFT let result: String = engine.eval(r#"LEFT("Hello World", 5)"#).unwrap(); assert_eq!(result, "Hello"); // RIGHT let result: String = engine.eval(r#"RIGHT("Hello World", 5)"#).unwrap(); assert_eq!(result, "World"); // MID let result: String = engine.eval(r#"MID("Hello World", 7, 5)"#).unwrap(); assert_eq!(result, "World"); } #[test] fn test_instr_function() { let engine = create_basic_engine(); let result: i64 = engine.eval(r#"INSTR("Hello World", "World")"#).unwrap(); assert_eq!(result, 7); // 1-based index let result: i64 = engine.eval(r#"INSTR("Hello World", "xyz")"#).unwrap(); assert_eq!(result, 0); // Not found let result: i64 = engine.eval(r#"INSTR("Hello World", "o")"#).unwrap(); assert_eq!(result, 5); // First occurrence } #[test] fn test_replace_function() { let engine = create_basic_engine(); let result: String = engine .eval(r#"REPLACE("Hello World", "World", "Rust")"#) .unwrap(); assert_eq!(result, "Hello Rust"); let result: String = engine.eval(r#"REPLACE("aaa", "a", "b")"#).unwrap(); assert_eq!(result, "bbb"); } // ============================================================================= // Math Function Tests with Engine // ============================================================================= #[test] fn test_math_operations_chain() { let engine = create_basic_engine(); // SQRT(ABS(-16)) let result: f64 = engine.eval("SQRT(ABS(-16.0))").unwrap(); assert!((result - 4.0).abs() < f64::EPSILON); // MAX(ABS(-5), ABS(-10)) let result: i64 = engine.eval("MAX(ABS(-5), ABS(-10))").unwrap(); assert_eq!(result, 10); } #[test] fn test_rounding_functions() { let engine = create_basic_engine(); // ROUND let result: i64 = engine.eval("ROUND(3.7)").unwrap(); assert_eq!(result, 4); let result: i64 = engine.eval("ROUND(3.2)").unwrap(); assert_eq!(result, 3); // FLOOR let result: i64 = engine.eval("FLOOR(3.9)").unwrap(); assert_eq!(result, 3); let result: i64 = engine.eval("FLOOR(-3.1)").unwrap(); assert_eq!(result, -4); // CEIL let result: i64 = engine.eval("CEIL(3.1)").unwrap(); assert_eq!(result, 4); let result: i64 = engine.eval("CEIL(-3.9)").unwrap(); assert_eq!(result, -3); } #[test] fn test_trigonometric_functions() { let engine = create_basic_engine(); let result: f64 = engine.eval("SIN(0.0)").unwrap(); assert!((result - 0.0).abs() < f64::EPSILON); let result: f64 = engine.eval("COS(0.0)").unwrap(); assert!((result - 1.0).abs() < f64::EPSILON); let pi: f64 = engine.eval("PI()").unwrap(); assert!((pi - std::f64::consts::PI).abs() < f64::EPSILON); } #[test] fn test_val_function() { let engine = create_basic_engine(); let result: f64 = engine.eval(r#"VAL("42")"#).unwrap(); assert!((result - 42.0).abs() < f64::EPSILON); let result: f64 = engine.eval(r#"VAL("3.14")"#).unwrap(); assert!((result - 3.14).abs() < f64::EPSILON); let result: f64 = engine.eval(r#"VAL("invalid")"#).unwrap(); assert!((result - 0.0).abs() < f64::EPSILON); } // ============================================================================= // TALK/HEAR Conversation Tests // ============================================================================= #[test] fn test_talk_output() { let output = OutputCollector::new(); let input = InputProvider::new(vec![]); let engine = create_conversation_engine(output.clone(), input); engine.eval::<()>(r#"TALK("Hello, World!")"#).unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 1); assert_eq!(messages[0], "Hello, World!"); } #[test] fn test_talk_multiple_messages() { let output = OutputCollector::new(); let input = InputProvider::new(vec![]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("Line 1"); TALK("Line 2"); TALK("Line 3"); "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 3); assert_eq!(messages[0], "Line 1"); assert_eq!(messages[1], "Line 2"); assert_eq!(messages[2], "Line 3"); } #[test] fn test_hear_input() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["Hello from user".to_string()]); let engine = create_conversation_engine(output, input); let result: String = engine.eval("HEAR()").unwrap(); assert_eq!(result, "Hello from user"); } #[test] fn test_talk_hear_conversation() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["John".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("What is your name?"); let name = HEAR(); TALK("Hello, " + name + "!"); "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 2); assert_eq!(messages[0], "What is your name?"); assert_eq!(messages[1], "Hello, John!"); } #[test] fn test_conditional_response() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["yes".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("Do you want to continue? (yes/no)"); let response = HEAR(); if UPPER(response) == "YES" { TALK("Great, let's continue!"); } else { TALK("Goodbye!"); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 2); assert_eq!(messages[1], "Great, let's continue!"); } #[test] fn test_keyword_detection() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["I need help with my order".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" let message = HEAR(); let upper_msg = UPPER(message); if INSTR(upper_msg, "HELP") > 0 { TALK("I can help you! What do you need?"); } else if INSTR(upper_msg, "ORDER") > 0 { TALK("Let me look up your order."); } else { TALK("How can I assist you today?"); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 1); // Should match "HELP" first since it appears before "ORDER" in the conditions assert_eq!(messages[0], "I can help you! What do you need?"); } // ============================================================================= // Variable and Expression Tests // ============================================================================= #[test] fn test_variable_assignment() { let engine = create_basic_engine(); let result: i64 = engine .eval( r#" let x = 10; let y = 20; let z = x + y; z "#, ) .unwrap(); assert_eq!(result, 30); } #[test] fn test_string_variables() { let engine = create_basic_engine(); let result: String = engine .eval( r#" let first_name = "John"; let last_name = "Doe"; let full_name = first_name + " " + last_name; UPPER(full_name) "#, ) .unwrap(); assert_eq!(result, "JOHN DOE"); } #[test] fn test_numeric_expressions() { let engine = create_basic_engine(); // Order of operations let result: i64 = engine.eval("2 + 3 * 4").unwrap(); assert_eq!(result, 14); let result: i64 = engine.eval("(2 + 3) * 4").unwrap(); assert_eq!(result, 20); // Using functions in expressions let result: i64 = engine.eval("ABS(-5) + MAX(3, 7)").unwrap(); assert_eq!(result, 12); } // ============================================================================= // Loop and Control Flow Tests // ============================================================================= #[test] fn test_for_loop() { let output = OutputCollector::new(); let input = InputProvider::new(vec![]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" for i in 1..4 { TALK("Count: " + i.to_string()); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 3); assert_eq!(messages[0], "Count: 1"); assert_eq!(messages[1], "Count: 2"); assert_eq!(messages[2], "Count: 3"); } #[test] fn test_while_loop() { let engine = create_basic_engine(); let result: i64 = engine .eval( r#" let count = 0; let sum = 0; while count < 5 { sum = sum + count; count = count + 1; } sum "#, ) .unwrap(); assert_eq!(result, 10); // 0 + 1 + 2 + 3 + 4 = 10 } // ============================================================================= // Error Handling Tests // ============================================================================= #[test] fn test_division_by_zero() { let engine = create_basic_engine(); // Rhai handles division by zero differently for int vs float let result = engine.eval::("10.0 / 0.0"); match result { Ok(val) => assert!(val.is_infinite() || val.is_nan()), Err(_) => (), // Division by zero error is also acceptable } } #[test] fn test_invalid_function_call() { let engine = create_basic_engine(); // Calling undefined function should error let result = engine.eval::(r#"UNDEFINED_FUNCTION("test")"#); assert!(result.is_err()); } #[test] fn test_type_mismatch() { let engine = create_basic_engine(); // Trying to use string where number expected let result = engine.eval::(r#"ABS("not a number")"#); assert!(result.is_err()); } // ============================================================================= // Script Fixture Tests // ============================================================================= #[test] fn test_greeting_script_logic() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["HELP".to_string()]); let engine = create_conversation_engine(output.clone(), input); // Simulated greeting script logic engine .eval::<()>( r#" let greeting = "Hello! Welcome to our service."; TALK(greeting); let user_input = HEAR(); if INSTR(UPPER(user_input), "HELP") > 0 { TALK("I can help you with: Products, Support, or Billing."); } else if INSTR(UPPER(user_input), "BYE") > 0 { TALK("Goodbye! Have a great day!"); } else { TALK("How can I assist you today?"); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 2); assert_eq!(messages[0], "Hello! Welcome to our service."); assert!(messages[1].contains("help")); } #[test] fn test_menu_flow_logic() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["1".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("Please select an option:"); TALK("1. Check order status"); TALK("2. Track shipment"); TALK("3. Contact support"); let choice = HEAR(); let choice_num = VAL(choice); if choice_num == 1.0 { TALK("Please enter your order number."); } else if choice_num == 2.0 { TALK("Please enter your tracking number."); } else if choice_num == 3.0 { TALK("Connecting you to support..."); } else { TALK("Invalid option. Please try again."); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 5); assert_eq!(messages[4], "Please enter your order number."); } #[test] fn test_echo_bot_logic() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["Hello".to_string(), "How are you?".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("Echo Bot: I will repeat what you say."); let input1 = HEAR(); TALK("You said: " + input1); let input2 = HEAR(); TALK("You said: " + input2); "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 3); assert_eq!(messages[0], "Echo Bot: I will repeat what you say."); assert_eq!(messages[1], "You said: Hello"); assert_eq!(messages[2], "You said: How are you?"); } // ============================================================================= // Complex Scenario Tests // ============================================================================= #[test] fn test_order_lookup_simulation() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["ORD-12345".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" TALK("Please enter your order number:"); let order_num = HEAR(); // Simulate order lookup let is_valid = INSTR(order_num, "ORD-") == 1 && LEN(order_num) >= 9; if is_valid { TALK("Looking up order " + order_num + "..."); TALK("Order Status: Shipped"); TALK("Estimated delivery: 3-5 business days"); } else { TALK("Invalid order number format. Please use ORD-XXXXX format."); } "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 4); assert!(messages[1].contains("ORD-12345")); assert!(messages[2].contains("Shipped")); } #[test] fn test_price_calculation() { let output = OutputCollector::new(); let input = InputProvider::new(vec!["3".to_string()]); let engine = create_conversation_engine(output.clone(), input); engine .eval::<()>( r#" let price = 29.99; TALK("Each widget costs $" + price.to_string()); TALK("How many would you like?"); let quantity = VAL(HEAR()); let subtotal = price * quantity; let tax = subtotal * 0.08; let total = subtotal + tax; TALK("Subtotal: $" + subtotal.to_string()); TALK("Tax (8%): $" + ROUND(tax * 100.0).to_string()); TALK("Total: $" + ROUND(total * 100.0).to_string()); "#, ) .unwrap(); let messages = output.get_messages(); assert_eq!(messages.len(), 5); assert!(messages[0].contains("29.99")); // Subtotal should be 89.97 assert!(messages[2].contains("89.97")); }