1291 lines · 44.7 KB · Rust
RawDownload
mod support; use agentos_execution::{    CreatePythonContextRequest, PythonExecutionEngine, PythonExecutionEvent, PythonExecutionLimits,    PythonVfsRpcMethod, PythonVfsRpcResponsePayload, PythonVfsRpcStat, StartPythonExecutionRequest,};use std::collections::BTreeMap;use std::fs;use std::path::{Path, PathBuf};use std::process::{Command, Stdio};use std::thread;use std::time::{Duration, Instant};use tempfile::tempdir; const PYTHON_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_PYTHON_WARMUP_METRICS__:";const PYTHON_EXECUTION_TIMEOUT_MS_ENV: &str = "AGENTOS_PYTHON_EXECUTION_TIMEOUT_MS";const PYTHON_MAX_OLD_SPACE_MB_ENV: &str = "AGENTOS_PYTHON_MAX_OLD_SPACE_MB";const PYTHON_OUTPUT_BUFFER_MAX_BYTES_ENV: &str = "AGENTOS_PYTHON_OUTPUT_BUFFER_MAX_BYTES";const PYTHON_VFS_RPC_TIMEOUT_MS_ENV: &str = "AGENTOS_PYTHON_VFS_RPC_TIMEOUT_MS"; #[derive(Debug, Clone, PartialEq)]struct PythonPrewarmMetrics {    executed: bool,    reason: String,    duration_ms: f64,    compile_cache_dir: String,    pyodide_dist_path: String,} #[derive(Debug, Clone, PartialEq)]struct PythonStartupMetrics {    prewarm_only: bool,    startup_ms: f64,    load_pyodide_ms: f64,    package_load_ms: f64,    package_count: usize,    source: String,} fn assert_node_available() {    let binary = std::env::var("AGENTOS_NODE_BINARY").unwrap_or_else(|_| String::from("node"));    let output = Command::new(binary)        .arg("--version")        .output()        .expect("spawn node --version");    assert!(output.status.success(), "node --version failed");} fn write_fixture(path: &Path, contents: &str) {    fs::write(path, contents).expect("write fixture");} fn write_pyodide_lock_fixture(path: &Path) {    write_fixture(path, "{\"packages\":[]}\n");    let pyodide_dir = path.parent().expect("pyodide fixture parent");    for asset in ["pyodide.asm.js", "pyodide.asm.wasm", "python_stdlib.zip"] {        let asset_path = pyodide_dir.join(asset);        if !asset_path.exists() {            fs::write(&asset_path, []).expect("write pyodide runtime fixture");        }    }} fn parse_metrics_line<'a>(stderr: &'a str, phase: &str) -> &'a str {    stderr        .lines()        .filter_map(|line| line.strip_prefix(PYTHON_WARMUP_METRICS_PREFIX))        .find(|line| parse_string_metric(line, "phase") == phase)        .unwrap_or_else(|| panic!("missing {phase} metrics line in stderr: {stderr}"))} fn parse_prewarm_metrics(stderr: &str) -> PythonPrewarmMetrics {    let metrics_line = parse_metrics_line(stderr, "prewarm");    PythonPrewarmMetrics {        executed: parse_boolean_metric(metrics_line, "executed"),        reason: parse_string_metric(metrics_line, "reason"),        duration_ms: parse_float_metric(metrics_line, "durationMs"),        compile_cache_dir: parse_string_metric(metrics_line, "compileCacheDir"),        pyodide_dist_path: parse_string_metric(metrics_line, "pyodideDistPath"),    }} fn parse_startup_metrics(stderr: &str) -> PythonStartupMetrics {    let metrics_line = parse_metrics_line(stderr, "startup");    PythonStartupMetrics {        prewarm_only: parse_boolean_metric(metrics_line, "prewarmOnly"),        startup_ms: parse_float_metric(metrics_line, "startupMs"),        load_pyodide_ms: parse_float_metric(metrics_line, "loadPyodideMs"),        package_load_ms: parse_float_metric(metrics_line, "packageLoadMs"),        package_count: parse_metric_value(metrics_line, "packageCount"),        source: parse_string_metric(metrics_line, "source"),    }} fn parse_metric_value(metrics_line: &str, key: &str) -> usize {    parse_float_metric(metrics_line, key) as usize} fn parse_float_metric(metrics_line: &str, key: &str) -> f64 {    let marker = format!("\"{key}\":");    let start = metrics_line.find(&marker).expect("metric key") + marker.len();    let digits: String = metrics_line[start..]        .chars()        .skip_while(|ch| !ch.is_ascii_digit() && *ch != '-')        .take_while(|ch| ch.is_ascii_digit() || matches!(ch, '.' | '-' | 'e' | 'E' | '+'))        .collect();     digits.parse().expect("float metric value")} fn parse_boolean_metric(metrics_line: &str, key: &str) -> bool {    let marker = format!("\"{key}\":");    let start = metrics_line.find(&marker).expect("metric key") + marker.len();    let remaining = &metrics_line[start..];     if remaining.starts_with("true") {        true    } else if remaining.starts_with("false") {        false    } else {        panic!("invalid boolean metric for {key}: {metrics_line}");    }} fn parse_string_metric(metrics_line: &str, key: &str) -> String {    let marker = format!("\"{key}\":\"");    let start = metrics_line.find(&marker).expect("metric key") + marker.len();    let mut value = String::new();    let mut escaped = false;     for ch in metrics_line[start..].chars() {        if escaped {            value.push(match ch {                'n' => '\n',                'r' => '\r',                't' => '\t',                '"' => '"',                '\\' => '\\',                other => other,            });            escaped = false;            continue;        }         match ch {            '\\' => escaped = true,            '"' => return value,            other => value.push(other),        }    }     panic!("unterminated string metric for {key}: {metrics_line}");} fn run_python_execution(    engine: &mut PythonExecutionEngine,    context_id: String,    cwd: &Path,    code: &str,    env: BTreeMap<String, String>,) -> (String, String, i32) {    let execution = engine        .start_execution(StartPythonExecutionRequest {            limits: Default::default(),            guest_runtime: Default::default(),            vm_id: String::from("vm-python"),            context_id,            code: String::from(code),            file_path: None,            env,            cwd: cwd.to_path_buf(),        })        .expect("start Python execution");     let result = execution.wait(None).expect("wait for Python execution");    let stdout = String::from_utf8(result.stdout).expect("stdout utf8");    let stderr = String::from_utf8(result.stderr).expect("stderr utf8");     (stdout, stderr, result.exit_code)} fn assert_process_exits(pid: u32) {    for _ in 0..20 {        let status = Command::new("kill")            .arg("-0")            .arg(pid.to_string())            .stdout(Stdio::null())            .stderr(Stdio::null())            .status()            .expect("probe process with kill -0");        if !status.success() {            return;        }        thread::sleep(Duration::from_millis(25));    }     panic!("process {pid} was still alive after waiting for cleanup");} fn python_contexts_preserve_vm_and_pyodide_configuration() {    let pyodide_dist_path = PathBuf::from("/tmp/pyodide");    let mut engine = support::python_engine();    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dist_path.clone(),    });     assert_eq!(context.context_id, "python-ctx-1");    assert_eq!(context.vm_id, "vm-python");    assert_eq!(context.pyodide_dist_path, pyodide_dist_path);} fn python_execution_runs_code_and_streams_stdio() {    assert_node_available();     let temp = tempdir().expect("create temp dir");    let pyodide_dir = temp.path().join("pyodide");    fs::create_dir_all(&pyodide_dir).expect("create pyodide dir");    write_fixture(        &pyodide_dir.join("pyodide.mjs"),        r#"export async function loadPyodide(options) {  return {    setStdin(_stdin) {},    async runPythonAsync(code) {      options.stdout(`stdout:${code}`);      options.stderr(`stderr:${options.indexURL}`);    },  };}"#,    );    write_pyodide_lock_fixture(&pyodide_dir.join("pyodide-lock.json"));     let mut engine = support::python_engine();    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dir.clone(),    });     let (stdout, stderr, exit_code) = run_python_execution(        &mut engine,        context.context_id,        temp.path(),        "print('hello')",        BTreeMap::new(),    );    assert_eq!(exit_code, 0);    assert_eq!(stdout, "stdout:print('hello')\n");    assert!(        stderr.starts_with("stderr:/__agentos_pyodide/"),        "unexpected stderr: {stderr}"    );} fn python_execution_wait_bounds_output_buffers() {    assert_node_available();     let temp = tempdir().expect("create temp dir");    let pyodide_dir = temp.path().join("pyodide");    fs::create_dir_all(&pyodide_dir).expect("create pyodide dir");    write_fixture(        &pyodide_dir.join("pyodide.mjs"),        r#"export async function loadPyodide(options) {  return {    setStdin(_stdin) {},    async runPythonAsync() {      options.stdout('x'.repeat(80));      options.stderr('y'.repeat(80));    },  };}"#,    );    write_pyodide_lock_fixture(&pyodide_dir.join("pyodide-lock.json"));     let mut engine = support::python_engine();    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dir,    });     let result = engine        .start_execution(StartPythonExecutionRequest {            guest_runtime: Default::default(),            // The cap is enforced from the typed wire limit. The env knob carries            // a much larger value to prove it is inert: if it were still read the            // buffer would not cap at 32.            limits: PythonExecutionLimits {                output_buffer_max_bytes: Some(32),                ..Default::default()            },            vm_id: String::from("vm-python"),            context_id: context.context_id,            code: String::from("print('ignored')"),            file_path: None,            env: BTreeMap::from([(                String::from(PYTHON_OUTPUT_BUFFER_MAX_BYTES_ENV),                String::from("999999"),            )]),            cwd: temp.path().to_path_buf(),        })        .expect("start Python execution")        .wait(None)        .expect("wait for Python execution");     assert_eq!(result.exit_code, 0);    assert_eq!(result.stdout.len(), 32, "stdout should be capped");    assert_eq!(result.stderr.len(), 32, "stderr should be capped");    assert!(result.stdout.iter().all(|byte| *byte == b'x'));    assert!(result.stderr.iter().all(|byte| *byte == b'y'));} fn python_execution_emits_stdout_before_exit() {    assert_node_available();     let temp = tempdir().expect("create temp dir");    let pyodide_dir = temp.path().join("pyodide");    fs::create_dir_all(&pyodide_dir).expect("create pyodide dir");    write_fixture(        &pyodide_dir.join("pyodide.mjs"),        r#"export async function loadPyodide(options) {  return {    setStdin(_stdin) {},    async runPythonAsync(code) {      options.stdout(`stdout:${code}`);    },  };}"#,    );    write_pyodide_lock_fixture(&pyodide_dir.join("pyodide-lock.json"));     let mut engine = support::python_engine();    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dir,    });     let mut execution = engine        .start_execution(StartPythonExecutionRequest {            guest_runtime: Default::default(),            limits: Default::default(),            vm_id: String::from("vm-python"),            context_id: context.context_id,            code: String::from("print('streamed')"),            file_path: None,            env: BTreeMap::new(),            cwd: temp.path().to_path_buf(),        })        .expect("start Python execution");     let mut saw_stdout = false;    let mut saw_exit = false;     while !saw_exit {        match execution            .poll_event_blocking(Duration::from_secs(5))            .expect("poll Python event")        {            Some(PythonExecutionEvent::Stdout(chunk)) => {                saw_stdout = String::from_utf8(chunk)                    .expect("stdout utf8")                    .contains("stdout:print('streamed')");            }            Some(PythonExecutionEvent::Exited(code)) => {                assert_eq!(code, 0);                saw_exit = true;            }            Some(PythonExecutionEvent::VfsRpcRequest(request)) => {                panic!("unexpected VFS RPC request during stdout test: {request:?}");            }            Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => {                // Module-resolution sync RPCs now surface here (the runner                // module imports node builtins); service them host-directly.                let serviced = execution                    .try_service_standalone_module_sync_rpc(&request)                    .expect("service module sync RPC");                assert!(                    serviced,                    "unexpected JS sync RPC request during stdout test: {request:?}"                );            }            Some(PythonExecutionEvent::Stderr(chunk)) => {                panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk));            }            None => panic!("timed out waiting for Python execution event"),        }    }     assert!(saw_stdout, "expected stdout event before exit");} fn python_execution_reports_prewarm_and_startup_metrics_when_debug_enabled() {    assert_node_available();     let temp = tempdir().expect("create temp dir");    let pyodide_dir = temp.path().join("pyodide");    fs::create_dir_all(&pyodide_dir).expect("create pyodide dir");    write_fixture(        &pyodide_dir.join("pyodide.mjs"),        r#"export async function loadPyodide() {  await new Promise((resolve) => setTimeout(resolve, 20));  return {    setStdin(_stdin) {},    async runPythonAsync(code) {      console.log(`ran:${code}`);    },  };}"#,    );    write_pyodide_lock_fixture(&pyodide_dir.join("pyodide-lock.json"));     let mut engine = support::python_engine();    let context = engine.create_context(CreatePythonContextRequest {        vm_id: String::from("vm-python"),        pyodide_dist_path: pyodide_dir.clone(),    });    let debug_env = BTreeMap::from([(        String::from("AGENTOS_PYTHON_WARMUP_DEBUG"),        String::from("1"),    )]);     let (first_stdout, first_stderr, first_exit_code) = run_python_execution(        &mut engine,        context.context_id.clone(),        temp.path(),        "print('first')",        debug_env.clone(),    );    let first_prewarm = parse_prewarm_metrics(&first_stderr);    let first_startup = parse_startup_metrics(&first_stderr);     assert_eq!(first_exit_code, 0);    assert!(first_stdout.contains("ran:print('first')"));    assert!(        first_prewarm.executed,        "first prewarm metrics: {first_prewarm:?}"    );    assert_eq!(first_prewarm.reason, "executed");    assert!(first_prewarm.duration_ms >= 0.0);    assert!(        first_prewarm.compile_cache_dir.contains("compile-cache"),        "unexpected prewarm metrics: {first_prewarm:?}"    );    assert_eq!(        PathBuf::from(&first_prewarm.pyodide_dist_path),        pyodide_dir,        "unexpected prewarm metrics: {first_prewarm:?}"    );    assert!(!first_startup.prewarm_only);    assert!(first_startup.startup_ms > 0.0);    assert!(first_startup.load_pyodide_ms > 0.0);    assert_eq!(first_startup.package_load_ms, 0.0);    assert_eq!(first_startup.package_count, 0);    assert_eq!(first_startup.source, "inline");     let (_second_stdout, second_stderr, second_exit_code) = run_python_execution(        &mut engine,        context.context_id,        temp.path(),        "print('second')",        debug_env,    );    let second_prewarm = parse_prewarm_metrics(&second_stderr);    let second_startup = parse_startup_metrics(&second_stderr);     assert_eq!(second_exit_code, 0);    assert!(        !second_prewarm.executed,        "second prewarm metrics: {second_prewarm:?}"    );    assert_eq!(second_prewarm.reason, "cached");    assert_eq!(second_prewarm.duration_ms, 0.0);    assert!(!second_startup.prewarm_only);    assert!(second_startup.startup_ms > 0.0);    assert!(second_startup.load_pyodide_ms > 0.0);    assert_eq!(second_startup.source, "inline");} fn python_execution_keeps_streaming_stdin_sessions_alive_until_closed() {    assert_node_available();     let temp = tempdir().expect("create temp dir");    let pyodide_dir = temp.path().join("pyodide");    fs::create_dir_all(&pyodide_dir).expect("create pyodide dir");    write_fixture(        &pyodide_dir.join("pyodide.mjs"),        r#"const decoder = new TextDecoder(); export async function loadPyodide(options) {  let stdin = null;  return {    setStdin(config) {      stdin = config;

Lines 1–500 of 1291.