//! Integration tests for Hero Runner //! //! Tests the hero runner by spawning the binary and dispatching jobs to it. //! //! **IMPORTANT**: Run with `--test-threads=1` to ensure tests run sequentially: //! ``` //! cargo test --test runner_hero -- --test-threads=1 //! ``` use hero_job::{Job, JobBuilder, JobStatus}; 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-hero-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 hero 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); }).ok(); }); // Use escargot to build and get the binary path let binary = escargot::CargoBuild::new() .bin("herorunner") .package("runner-hero") .run() .expect("Failed to build hero runner binary"); // Start the runner binary let child = binary .command() .args(&[ RUNNER_ID, "--redis-url", REDIS_URL, ]) .spawn() .expect("Failed to start hero runner"); *RUNNER_PROCESS.lock().unwrap() = Some(child); // Wait for runner to be ready with TCP check use std::time::Duration; std::thread::sleep(Duration::from_secs(2)); println!("✅ Hero runner ready"); }); } /// Cleanup runner process fn cleanup_runner() { if let Ok(mut guard) = RUNNER_PROCESS.lock() { if let Some(mut child) = guard.take() { println!("🧹 Cleaning up hero runner process..."); let _ = child.kill(); let _ = child.wait(); } } } /// Helper to create a test client async fn create_client() -> Client { // Ensure runner is running 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") .context_id("test-context") .payload(payload) .runner(RUNNER_ID) .timeout(30) .build() .expect("Failed to build job") } #[tokio::test] async fn test_01_ping_job() { println!("\n🧪 Test: Ping Job"); let client = create_client().await; // Create ping job let job = create_test_job("ping"); let job_id = job.id.clone(); // Save job to Redis client.store_job_in_redis(&job).await.expect("Failed to save job"); // Queue job to runner 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(2)).await; // Check job status let status = client.get_status(&job_id).await.expect("Failed to get job status"); assert_eq!(status, JobStatus::Finished, "Ping job should be finished"); // Check result let result = client.get_result(&job_id).await.expect("Failed to get result"); assert_eq!(result, Some("pong".to_string()), "Ping should return pong"); println!("✅ Ping job completed successfully"); } #[tokio::test] async fn test_02_simple_heroscript() { println!("\n🧪 Test: Simple Heroscript"); let client = create_client().await; // Create job with simple heroscript let job = create_test_job("print('Hello from hero runner')"); 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 if let Some(result) = client.get_result(&job_id).await.expect("Failed to get result") { println!("Job result: {}", result); } if let Some(error) = client.get_error(&job_id).await.expect("Failed to get error") { println!("Job error: {}", error); } println!("✅ Heroscript job completed"); } #[tokio::test] async fn test_03_job_with_env_vars() { println!("\n🧪 Test: Job with Environment Variables"); let client = create_client().await; // Create job with env vars let mut job = create_test_job("echo $TEST_VAR"); job.env_vars.insert("TEST_VAR".to_string(), "test_value".to_string()); 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 if let Some(result) = client.get_result(&job_id).await.expect("Failed to get result") { println!("Job result: {}", result); } println!("✅ Job with env vars completed"); } #[tokio::test] async fn test_04_job_timeout() { println!("\n🧪 Test: Job Timeout"); let client = create_client().await; // Create job with short timeout let mut job = create_test_job("sleep 10"); job.timeout = 2; // 2 second timeout 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 timeout tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; // Check job status - should be error due to timeout 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 timeout): {}", error); assert!(error.contains("timeout") || error.contains("timed out"), "Error should mention timeout"); } println!("✅ Job timeout handled correctly"); } /// 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"); }