refactor: Overhaul Rhai scripting with multi-file hot reloading

This commit represents a major refactoring of our Rhai scripting system,
transforming it from a factory-based approach to a more robust system-based
architecture with improved hot reloading capabilities.

Key Changes:
- Renamed package from rhai_factory to rhai_system to better reflect its purpose
- Renamed system_factory.rs to factory.rs for consistency and clarity
- Implemented support for multiple script files in hot reloading
- Added cross-script function calls, allowing functions in one script to call functions in another
- Improved file watching to monitor all script files for changes
- Enhanced error handling for script compilation failures
- Simplified the API with a cleaner create_hot_reloadable_system function
- Removed unused modules (error.rs, factory.rs, hot_reload_old.rs, module_cache.rs, relative_resolver.rs)
- Updated all tests to work with the new architecture

The new architecture:
- Uses a System struct that holds references to script paths and provides a clean API
- Compiles and merges multiple Rhai script files into a single AST
- Automatically detects changes to any script file and recompiles them
- Maintains thread safety with proper synchronization primitives
- Provides better error messages when scripts fail to compile

This refactoring aligns with our BasePathModuleResolver approach for module imports,
making the resolution process more predictable and consistent. The hot reload example
has been updated to demonstrate the new capabilities, showing how to:
1. Load and execute multiple script files
2. Watch for changes to these files
3. Automatically reload scripts when they change
4. Call functions across different script files

All tests are passing, and the example demonstrates the improved functionality.
This commit is contained in:
Timur Gordon
2025-05-02 21:04:33 +02:00
parent 939b6b4e57
commit 372b7a2772
54 changed files with 5692 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
//! Common utilities for integration tests.
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rand;
/// Test fixture for integration tests.
pub struct TestFixture {
pub scripts_dir: PathBuf,
}
impl TestFixture {
/// Create a new test fixture.
pub fn new() -> Self {
let scripts_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("rhai_scripts");
Self {
scripts_dir,
}
}
/// Get the path to a test script.
pub fn script_path(&self, name: &str) -> PathBuf {
self.scripts_dir.join(name)
}
}
/// Create a temporary test directory with a unique name.
pub fn setup_test_dir(prefix: &str) -> PathBuf {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("temp")
.join(format!("{}_{}_{}", prefix, timestamp, rand::random::<u16>()));
fs::create_dir_all(&test_dir).unwrap();
test_dir
}
/// Clean up a temporary test directory.
pub fn cleanup_test_dir(dir: PathBuf) {
if dir.exists() && dir.starts_with(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests").join("temp")) {
let _ = fs::remove_dir_all(dir);
}
}

View File

@@ -0,0 +1,125 @@
use std::fs;
use std::sync::{Arc, RwLock};
use rhai::Engine;
use rhai_system::{System, hot_reload_callback};
#[test]
fn test_hot_reload_callback() {
// Create temporary script files
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test_script.rhai");
// Write initial script content
let initial_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the original script."
}
fn add(a, b) {
a + b
}
"#;
fs::write(&script_path, initial_script).unwrap();
// Create engine and compile initial script
let engine = Engine::new();
let ast = engine.compile_file(script_path.clone()).unwrap();
let shared_ast = Arc::new(RwLock::new(ast));
// Create a system with the initial script
let script_paths = vec![script_path.clone()];
let system = System::new(engine, shared_ast, script_paths);
// Test initial script functionality
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
let add_result: i32 = system.call_fn("add", (40, 2)).unwrap();
assert_eq!(add_result, 42);
// Write modified script content with new functions
let modified_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script!"
}
fn add(a, b) {
a + b
}
fn multiply(a, b) {
a * b
}
"#;
fs::write(&script_path, modified_script).unwrap();
// Call the hot reload callback
hot_reload_callback(&system, script_path.to_str().unwrap()).unwrap();
// Test that the script was updated
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the MODIFIED script!");
// Test that the new function is available
let multiply_result: i32 = system.call_fn("multiply", (6, 7)).unwrap();
assert_eq!(multiply_result, 42);
}
#[test]
fn test_hot_reload_callback_with_syntax_error() {
// Create temporary script files
let temp_dir = tempfile::tempdir().unwrap();
let script_path = temp_dir.path().join("test_script.rhai");
// Write initial script content
let initial_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the original script."
}
fn add(a, b) {
a + b
}
"#;
fs::write(&script_path, initial_script).unwrap();
// Create engine and compile initial script
let engine = Engine::new();
let ast = engine.compile_file(script_path.clone()).unwrap();
let shared_ast = Arc::new(RwLock::new(ast));
// Create a system with the initial script
let script_paths = vec![script_path.clone()];
let system = System::new(engine, shared_ast, script_paths);
// Test initial script functionality
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
// Write modified script content with syntax error
let modified_script = r#"
fn greet(name) {
"Hello, " + name + "! This is the MODIFIED script!"
}
fn add(a, b) {
a + b
}
fn syntax_error() {
// Missing closing brace
if (true) {
"This will cause a syntax error"
}
"#;
fs::write(&script_path, modified_script).unwrap();
// Call the hot reload callback - it should return an error
let result = hot_reload_callback(&system, script_path.to_str().unwrap());
assert!(result.is_err());
// Test that the original script functionality is still available
let result: String = system.call_fn("greet", ("User",)).unwrap();
assert_eq!(result, "Hello, User! This is the original script.");
}

View File

@@ -0,0 +1,9 @@
// main.rhai - Main script that imports module1
import "module1" as m1;
// Call the calculate function from module1, which in turn calls multiply from module2
let answer = m1::calculate();
// Return the answer
answer

View File

@@ -0,0 +1,9 @@
// module1.rhai - A simple module that imports module2
import "module2" as m2;
fn calculate() {
// Call the multiply function from module2
let result = m2::multiply(6, 7);
result
}

View File

@@ -0,0 +1,5 @@
// module2.rhai - A simple module with a multiply function
fn multiply(x, y) {
x * y
}

View File

@@ -0,0 +1 @@
fn get_value() { 84 ; ; }

View File

@@ -0,0 +1 @@
fn get_value() { 84 ; ; }

View File

@@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@@ -0,0 +1,2 @@
import "module" as m;
fn calculate() { 21 * m::get_multiplier() }

View File

@@ -0,0 +1 @@
fn get_multiplier() { 4 }

View File

@@ -0,0 +1,2 @@
fn get_multiplier() { 4 }
fn calculate() { 21 * get_multiplier() }

View File

@@ -0,0 +1 @@
// This is a secondary file that will be modified

View File

@@ -0,0 +1,2 @@
fn get_multiplier() { 4 }
fn calculate() { 21 * get_multiplier() }

View File

@@ -0,0 +1 @@
// This is a secondary file that will be modified

View File

@@ -0,0 +1 @@
fn get_value() { 84 }

View File

@@ -0,0 +1 @@
fn get_value() { 84 }