//! Integration tests for SAL Runner //! //! Tests the SAL runner by spawning the binary and dispatching Rhai jobs to it. //! //! **IMPORTANT**: Run with `--test-threads=1` to ensure tests run sequentially: //! ``` //! cargo test --test runner_sal -- --test-threads=1 //! ``` use hero_job::{Job, JobBuilder}; use hero_job_client::Client; use std::sync::{Mutex, Once}; use std::process::Child; use lazy_static::lazy_static; /// Test configuration const RUNNER_ID: &str = "test-sal-runner"; const REDIS_URL: &str = "redis://localhost:6379"; lazy_static! { static ref RUNNER_PROCESS: Mutex> = Mutex::new(None); } /// Global initialization flag static INIT: Once = Once::new(); /// Initialize and start the SAL runner binary async fn init_runner() { INIT.call_once(|| { // Register cleanup handler let _ = std::panic::catch_unwind(|| { ctrlc::set_handler(move || { cleanup_runner(); std::process::exit(0); }).expect("Error setting Ctrl-C handler"); }); println!("๐Ÿš€ Starting SAL runner..."); // Build the runner binary let build_result = escargot::CargoBuild::new() .bin("runner_sal") .package("runner-sal") .current_release() .run() .expect("Failed to build runner_sal"); // Spawn the runner process let child = build_result .command() .arg(RUNNER_ID) .arg("--redis-url") .arg(REDIS_URL) .arg("--db-path") .arg("/tmp/test_sal.db") .spawn() .expect("Failed to spawn SAL runner"); *RUNNER_PROCESS.lock().unwrap() = Some(child); // Give the runner time to start std::thread::sleep(std::time::Duration::from_secs(2)); println!("โœ… SAL runner ready"); }); } /// Cleanup runner process fn cleanup_runner() { println!("๐Ÿงน Cleaning up SAL runner process..."); if let Some(mut child) = RUNNER_PROCESS.lock().unwrap().take() { let _ = child.kill(); let _ = child.wait(); } // Clean up test database let _ = std::fs::remove_file("/tmp/test_sal.db"); } /// Create a test job client async fn create_client() -> Client { init_runner().await; Client::builder() .redis_url(REDIS_URL) .build() .await .expect("Failed to create job client") } /// Helper to create a test job fn create_test_job(payload: &str) -> Job { JobBuilder::new() .caller_id("test-caller") .context_id("test-context") .runner(RUNNER_ID) .payload(payload) .timeout(30) .build() .expect("Failed to build test job") } #[tokio::test] async fn test_01_simple_rhai_script() { println!("\n๐Ÿงช Test: Simple Rhai Script"); let client = create_client().await; // Create job with simple Rhai script let job = create_test_job(r#" let message = "Hello from SAL"; print(message); message "#); let job_id = job.id.clone(); // Save and queue job match client.job_run_wait(&job, RUNNER_ID, 5).await { Ok(result) => { println!("โœ… Job succeeded with result:\n{}", result); assert!(result.contains("Hello from SAL") || result.contains("SAL"), "Result should contain message"); } Err(e) => { println!("โŒ Job failed with error: {:?}", e); panic!("Job execution failed"); } } println!("โœ… Rhai script job completed"); } #[tokio::test] async fn test_02_rhai_array_operations() { println!("\n๐Ÿงช Test: Rhai Array Operations"); let client = create_client().await; // Create job with array operations let job = create_test_job(r#" let arr = [1, 2, 3, 4, 5]; let sum = 0; for item in arr { sum += item; } print("Sum: " + sum); sum "#); let job_id = job.id.clone(); // Save and queue job client.store_job_in_redis(&job).await.expect("Failed to save job"); client.job_run(&job_id, RUNNER_ID).await.expect("Failed to queue job"); // Wait for job to complete tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Check job status let status = client.get_status(&job_id).await.expect("Failed to get job status"); println!("๐Ÿ“Š Job status: {:?}", status); // Get result or error match (client.get_result(&job_id).await, client.get_error(&job_id).await) { (Ok(Some(result)), _) => { println!("โœ… Job succeeded with result:\n{}", result); assert!(result.contains("15") || result.contains("Sum"), "Result should contain sum of 15"); } (_, Ok(Some(error))) => { println!("โŒ Job failed with error:\n{}", error); panic!("Job should have succeeded"); } _ => { println!("โš ๏ธ No result or error available"); panic!("Expected result"); } } println!("โœ… Array operations job completed"); } #[tokio::test] async fn test_03_rhai_object_operations() { println!("\n๐Ÿงช Test: Rhai Object Operations"); let client = create_client().await; // Create job with object/map operations let job = create_test_job(r#" let obj = #{ name: "Test", value: 42, active: true }; print("Name: " + obj.name); print("Value: " + obj.value); obj.value "#); let job_id = job.id.clone(); // Save and queue job client.store_job_in_redis(&job).await.expect("Failed to save job"); client.job_run(&job_id, RUNNER_ID).await.expect("Failed to queue job"); // Wait for job to complete tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Check job status let status = client.get_status(&job_id).await.expect("Failed to get job status"); println!("๐Ÿ“Š Job status: {:?}", status); // Get result or error match (client.get_result(&job_id).await, client.get_error(&job_id).await) { (Ok(Some(result)), _) => { println!("โœ… Job succeeded with result:\n{}", result); assert!(result.contains("42"), "Result should contain value 42"); } (_, Ok(Some(error))) => { println!("โŒ Job failed with error:\n{}", error); panic!("Job should have succeeded"); } _ => { println!("โš ๏ธ No result or error available"); panic!("Expected result"); } } println!("โœ… Object operations job completed"); } #[tokio::test] async fn test_04_invalid_rhai_syntax() { println!("\n๐Ÿงช Test: Invalid Rhai Syntax Error Handling"); let client = create_client().await; // Create job with invalid Rhai syntax let job = create_test_job("fn broken( { // Invalid syntax"); let job_id = job.id.clone(); // Save and queue job client.store_job_in_redis(&job).await.expect("Failed to save job"); client.job_run(&job_id, RUNNER_ID).await.expect("Failed to queue job"); // Wait for job to complete tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Check job status - should be error let status = client.get_status(&job_id).await.expect("Failed to get job status"); println!("๐Ÿ“Š Job status: {:?}", status); // Should have error if let Some(error) = client.get_error(&job_id).await.expect("Failed to get error") { println!("โŒ Job error (expected):\n{}", error); println!("โœ… Invalid Rhai syntax error handled correctly"); } else { println!("โš ๏ธ Expected error for invalid Rhai syntax but got none"); panic!("Job with invalid syntax should have failed"); } } /// Final test that ensures cleanup happens #[tokio::test] async fn test_zz_cleanup() { println!("\n๐Ÿงน Running cleanup..."); cleanup_runner(); // Wait a bit to ensure process is killed tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; println!("โœ… Cleanup complete"); }