194 lines · 7.8 KB · Rust
RawDownload
mod support; use agentos_execution::{    CreatePythonContextRequest, PythonExecutionEngine, PythonExecutionEvent,    StartPythonExecutionRequest,};use serde_json::Value;use std::collections::BTreeMap;use std::fs;use std::path::{Path, PathBuf};use std::time::Duration;use tempfile::tempdir; const PYTHON_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_PYTHON_WARMUP_METRICS__:"; fn setup_engine() -> (PythonExecutionEngine, String, PathBuf) {    let mut engine = support::python_engine();    let pyodide_dir = engine        .bundled_pyodide_dist_path_for_vm("vm-python")        .expect("materialize bundled pyodide");    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dir.clone(),    });    (engine, context.context_id, pyodide_dir)} fn run_python_execution(    engine: &mut PythonExecutionEngine,    context_id: &str,    cwd: &Path,    code: &str,) -> (String, String, i32) {    let mut execution = engine        .start_execution(StartPythonExecutionRequest {            guest_runtime: Default::default(),            limits: Default::default(),            vm_id: String::from("vm-python"),            context_id: context_id.to_string(),            code: code.to_string(),            file_path: None,            env: BTreeMap::from([(                String::from("AGENTOS_PYTHON_WARMUP_DEBUG"),                String::from("1"),            )]),            cwd: cwd.to_path_buf(),        })        .expect("start Python execution");     // Drive the event loop directly instead of `.wait()`: the Pyodide runner    // now sets up a kernel-VFS-backed site-packages on boot, which emits VFS    // RPCs. This prewarm test has no VFS backend, so reject those RPCs and let    // the runner's best-effort site-packages setup degrade. Module-resolution    // sync RPCs are serviced host-directly.    let mut stdout = Vec::new();    let mut stderr = Vec::new();    loop {        match execution            .poll_event_blocking(Duration::from_secs(60))            .expect("poll Python event")        {            Some(PythonExecutionEvent::Stdout(chunk)) => stdout.extend(chunk),            Some(PythonExecutionEvent::Stderr(chunk)) => stderr.extend(chunk),            Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => {                let serviced = execution                    .try_service_standalone_module_sync_rpc(&request)                    .expect("service module sync RPC");                assert!(serviced, "unexpected JS sync RPC request: {request:?}");            }            Some(PythonExecutionEvent::VfsRpcRequest(request)) => {                execution                    .respond_vfs_rpc_error(request.id, "ENOSYS", "no VFS backend in prewarm test")                    .expect("respond to VFS RPC");            }            Some(PythonExecutionEvent::Exited(exit_code)) => {                return (                    String::from_utf8(stdout).expect("stdout utf8"),                    String::from_utf8(stderr).expect("stderr utf8"),                    exit_code,                );            }            None => panic!("timed out waiting for Python execution event"),        }    }} fn parse_metrics(stderr: &str, phase: &str) -> Value {    let payload = stderr        .lines()        .filter_map(|line| line.strip_prefix(PYTHON_WARMUP_METRICS_PREFIX))        .map(|line| serde_json::from_str::<Value>(line).expect("parse metrics json"))        .find(|value| value.get("phase").and_then(Value::as_str) == Some(phase))        .unwrap_or_else(|| panic!("missing {phase} metrics in stderr: {stderr}"));    payload} fn python_execution_prewarms_once_when_compile_cache_is_ready() {    let temp = tempdir().expect("create temp dir");    let (mut engine, context_id, _pyodide_dir) = setup_engine();    let (first_stdout, first_stderr, first_exit_code) =        run_python_execution(&mut engine, &context_id, temp.path(), "print('first')");    let (second_stdout, second_stderr, second_exit_code) =        run_python_execution(&mut engine, &context_id, temp.path(), "print('second')");     assert_eq!(first_exit_code, 0, "stderr: {first_stderr}");    assert_eq!(second_exit_code, 0, "stderr: {second_stderr}");    assert_eq!(first_stdout, "first\n");    assert_eq!(second_stdout, "second\n");     let first_prewarm = parse_metrics(&first_stderr, "prewarm");    let second_prewarm = parse_metrics(&second_stderr, "prewarm");     assert_eq!(first_prewarm["executed"], true);    assert_eq!(first_prewarm["reason"], "executed");    assert_eq!(second_prewarm["executed"], false);    assert_eq!(second_prewarm["reason"], "cached");} fn python_execution_invalidates_prewarm_stamp_when_pyodide_bundle_changes() {    let temp = tempdir().expect("create temp dir");    let (mut engine, context_id, pyodide_dir) = setup_engine();    let pyodide_mjs = pyodide_dir.join("pyodide.mjs");    let (_first_stdout, first_stderr, first_exit_code) =        run_python_execution(&mut engine, &context_id, temp.path(), "print('first')");    assert_eq!(first_exit_code, 0, "stderr: {first_stderr}");    assert_eq!(        parse_metrics(&first_stderr, "prewarm")["reason"],        "executed"    );     let original = fs::read_to_string(&pyodide_mjs).expect("read pyodide module");    fs::write(        &pyodide_mjs,        format!("{original}\n// prewarm invalidation test\n"),    )    .expect("mutate pyodide module");     let (second_stdout, second_stderr, second_exit_code) =        run_python_execution(&mut engine, &context_id, temp.path(), "print('second')");    assert_eq!(second_exit_code, 0, "stderr: {second_stderr}");    assert_eq!(second_stdout, "second\n");    assert_eq!(        parse_metrics(&second_stderr, "prewarm")["reason"],        "executed"    );} // Separate libtest cases in this binary still trip a V8 teardown/init crash, so// keep the prewarm coverage in one top-level suite until that boundary is fixed.//// cargo-nextest runs every `#[test]` in its OWN process, so that crash cannot// occur there — `mod python_prewarm_split` below exposes each case as a separate// test that nextest runs in parallel across cores. Skip the collapsed run under// nextest so the work isn't done twice; the split cases skip themselves under// `cargo test`. nextest sets `NEXTEST=1` in each test process; libtest/`cargo// test` does not.#[test]fn python_prewarm_suite() {    if std::env::var_os("NEXTEST").is_some() {        return;    }    python_execution_prewarms_once_when_compile_cache_is_ready();    python_execution_invalidates_prewarm_stamp_when_pyodide_bundle_changes();} /// Per-case split of `python_prewarm_suite` for cargo-nextest (process-per-test).////// Each `#[test]` here runs exactly one Pyodide prewarm case in its own process,/// so the shared-process V8/Pyodide teardown/init crash that forces the collapsed/// `python_prewarm_suite` above does not apply, and the cases run in parallel/// across cores instead of serially. Each case skips itself under plain `cargo/// test` (where `NEXTEST` is unset) so the collapsed suite owns the run there; the/// collapsed suite likewise skips under nextest so the work isn't duplicated.mod python_prewarm_split {    macro_rules! nextest_cases {        ($($case:ident),+ $(,)?) => {            $(                #[test]                fn $case() {                    // Covered by the collapsed `super::python_prewarm_suite` under `cargo test`.                    if std::env::var_os("NEXTEST").is_none() {                        return;                    }                    super::$case();                }            )+        };    }     nextest_cases!(        python_execution_prewarms_once_when_compile_cache_is_ready,        python_execution_invalidates_prewarm_stamp_when_pyodide_bundle_changes,    );}