//! Integration tests for Osiris Runner (OSIS) //! //! Tests the osiris 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_osiris -- --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-osiris-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 osiris 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 Osiris runner..."); // Build the runner binary let build_result = escargot::CargoBuild::new() .bin("runner_osiris") .package("runner-osiris") .current_release() .run() .expect("Failed to build runner_osiris"); // Spawn the runner process let child = build_result .command() .arg(RUNNER_ID) .arg("--redis-url") .arg(REDIS_URL) .spawn() .expect("Failed to spawn osiris runner"); *RUNNER_PROCESS.lock().unwrap() = Some(child); // Give the runner time to start std::thread::sleep(std::time::Duration::from_secs(2)); println!("โœ… Osiris runner ready"); }); } /// Cleanup runner process fn cleanup_runner() { println!("๐Ÿงน Cleaning up osiris runner process..."); if let Some(mut child) = RUNNER_PROCESS.lock().unwrap().take() { let _ = child.kill(); let _ = child.wait(); } } /// 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 x = 10; let y = 20; print("Sum: " + (x + y)); x + y "#); // 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("30") || result.contains("Sum"), "Result should contain calculation"); } Err(e) => { println!("โŒ Job failed with error: {:?}", e); panic!("Job execution failed"); } } println!("โœ… Rhai script job completed"); } #[tokio::test] async fn test_02_rhai_with_functions() { println!("\n๐Ÿงช Test: Rhai Script with Functions"); let client = create_client().await; // Create job with Rhai function let job = create_test_job(r#" fn calculate(a, b) { a * b + 10 } let result = calculate(5, 3); print("Result: " + result); result "#); 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("25") || result.contains("Result"), "Result should contain 25"); } (_, 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!("โœ… Rhai function job completed"); } #[tokio::test] async fn test_03_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("let x = ; // 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"); }