bottest/tests/integration/basic_runtime.rs

729 lines
21 KiB
Rust
Raw Normal View History

2025-12-06 11:05:57 -03:00
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::<f64>().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::<i64>().is_ok() || trimmed.parse::<f64>().is_ok()
});
engine
}
/// Mock output collector for TALK commands
#[derive(Clone, Default)]
struct OutputCollector {
messages: Arc<Mutex<Vec<String>>>,
}
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<String> {
self.messages.lock().unwrap().clone()
}
}
/// Mock input provider for HEAR commands
#[derive(Clone)]
struct InputProvider {
inputs: Arc<Mutex<Vec<String>>>,
index: Arc<Mutex<usize>>,
}
impl InputProvider {
fn new(inputs: Vec<String>) -> 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::<f64>("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::<String>(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::<i64>(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"));
}