/* * Tests for the PrewarmedIsolate used by nativets dedicated workers. * * Run with: * cargo test -p windmill --features "deno_core" --test nativets_dedicated -- --nocapture */ #[cfg(feature = "deno_core")] mod prewarmed_isolate_tests { use std::process::Command; use windmill_runtime_nativets::{NativeAnnotation, PrewarmedIsolate}; use windmill_worker::{build_loader, LoaderMode, BUN_PATH}; fn default_annotation() -> NativeAnnotation { NativeAnnotation { useragent: None, proxy: None } } /// Bundle a TypeScript script into JS suitable for `PrewarmedIsolate`. /// /// Returns `(ts_source, js_bundle, arg_names)`. async fn bundle_script(script: &str) -> (String, String, Vec) { let temp_dir = tempfile::tempdir().unwrap(); let dir = temp_dir.path(); let dir_str = dir.to_str().unwrap(); std::fs::write(dir.join("main.ts"), script).unwrap(); build_loader( dir_str, "http://localhost:8000", "test_token", "test-workspace", "f/test/script", LoaderMode::BrowserBundle, &None, ) .await .expect("build_loader failed"); let output = Command::new(BUN_PATH.as_str()) .args(["run", dir.join("node_builder.ts").to_str().unwrap()]) .current_dir(dir) .output() .expect("Failed to run bun build"); if !output.status.success() { panic!( "Bun build failed: {}", String::from_utf8_lossy(&output.stderr) ); } let ts = std::fs::read_to_string(dir.join("main.ts")).unwrap(); let js = std::fs::read_to_string(dir.join("main.js")).unwrap(); let parsed = windmill_parser_ts::parse_deno_signature(&ts, true, false, None) .expect("failed to parse signature"); let arg_names: Vec = parsed.args.into_iter().map(|a| a.name).collect(); (ts, js, arg_names) } const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); async fn run_prewarmed_test( script: &str, jobs: Vec, ) -> Vec> { tokio::time::timeout(TEST_TIMEOUT, run_prewarmed_test_inner(script, jobs)) .await .expect("test timed out after 30s") } async fn run_prewarmed_test_inner( script: &str, jobs: Vec, ) -> Vec> { windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed"); let (_ts, js, arg_names) = bundle_script(script).await; let ann = default_annotation(); let mut results = Vec::new(); for job_args in &jobs { let mut isolate = PrewarmedIsolate::spawn( "".to_string(), js.clone(), ann.clone(), arg_names.clone(), None, ); isolate.wait_ready().await.expect("isolate failed to warm"); let args = serde_json::to_string(job_args).unwrap(); let executing = isolate.start_execution(args); let prewarmed_result = executing.wait().await.expect("isolate execution failed"); match prewarmed_result.result { Ok(raw) => { let value: serde_json::Value = serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null); results.push(Ok(value)); } Err(e) => { results.push(Err(e)); } } } results } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_simple() { let script = r#" export function main(x: number, y: number): number { return x + y; } "#; let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 2, "y": 3})]).await; assert_eq!(results.len(), 1); assert_eq!(results[0], Ok(serde_json::json!(5))); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_isolation() { let script = r#" let counter = 0; export function main(): number { counter++; return counter; } "#; let results = run_prewarmed_test(script, vec![serde_json::json!({}), serde_json::json!({})]).await; assert_eq!(results.len(), 2); // Each job gets a fresh isolate, so counter should be 1 both times assert_eq!(results[0], Ok(serde_json::json!(1))); assert_eq!(results[1], Ok(serde_json::json!(1))); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_error() { let script = r#" export function main(msg: string): never { throw new Error(msg); } "#; let results = run_prewarmed_test(script, vec![serde_json::json!({"msg": "test error"})]).await; assert_eq!(results.len(), 1); assert!(results[0].is_err()); assert!( results[0].as_ref().unwrap_err().contains("test error"), "Error should contain 'test error', got: {}", results[0].as_ref().unwrap_err() ); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_async() { let script = r#" export async function main(x: number): Promise { const val = await Promise.resolve(x * 10); return val + 1; } "#; let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 7})]).await; assert_eq!(results.len(), 1); assert_eq!(results[0], Ok(serde_json::json!(71))); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_pipeline() { windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed"); let script = r#" export function main(n: number): number { return n * 2; } "#; let (_ts, js, arg_names) = bundle_script(script).await; let ann = default_annotation(); // Pre-warm first isolate let mut warm = PrewarmedIsolate::spawn( "".to_string(), js.clone(), ann.clone(), arg_names.clone(), None, ); warm.wait_ready() .await .expect("first isolate failed to warm"); let mut results = Vec::new(); for i in 1..=3 { let args = serde_json::to_string(&serde_json::json!({"n": i})).unwrap(); let executing = warm.start_execution(args); // Pipeline: start pre-warming next isolate while current one runs warm = PrewarmedIsolate::spawn( "".to_string(), js.clone(), ann.clone(), arg_names.clone(), None, ); let prewarmed_result = executing.wait().await.expect("isolate execution failed"); match prewarmed_result.result { Ok(raw) => { let value: serde_json::Value = serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null); results.push(value); } Err(e) => panic!("unexpected error: {e}"), } warm.wait_ready() .await .expect("next isolate failed to warm"); } assert_eq!( results, vec![ serde_json::json!(2), serde_json::json!(4), serde_json::json!(6), ] ); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_complex_return() { let script = r#" export function main(name: string, items: number[]): any { return { greeting: `hello ${name}`, sum: items.reduce((a, b) => a + b, 0), items: items.map(x => x * 2), }; } "#; let results = run_prewarmed_test( script, vec![serde_json::json!({"name": "world", "items": [1, 2, 3]})], ) .await; assert_eq!(results.len(), 1); assert_eq!( results[0], Ok(serde_json::json!({ "greeting": "hello world", "sum": 6, "items": [2, 4, 6], })) ); } #[tokio::test(flavor = "multi_thread")] async fn test_prewarmed_null_undefined() { let script = r#" export function main(returnNull: boolean): any { if (returnNull) { return null; } return undefined; } "#; let results = run_prewarmed_test( script, vec![ serde_json::json!({"returnNull": true}), serde_json::json!({"returnNull": false}), ], ) .await; assert_eq!(results.len(), 2); assert_eq!(results[0], Ok(serde_json::Value::Null)); assert_eq!(results[1], Ok(serde_json::Value::Null)); } }