#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "preinit")]
use eryx::Sandbox;
use eryx::preinit::pre_initialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::{Mutex, OnceCell};
fn get_stdlib_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("eryx-wasm-runtime/tests/python-stdlib")
}
static PREINIT_CACHE: OnceCell<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceCell::const_new();
fn cache_key(imports: &[&str]) -> String {
if imports.is_empty() {
"default".to_string()
} else {
format!("imports-{}", imports.join("-"))
}
}
fn cache_dir() -> PathBuf {
let run_id =
std::env::var("NEXTEST_RUN_ID").unwrap_or_else(|_| format!("pid-{}", std::process::id()));
Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("preinit-cache")
.join(run_id)
}
fn prune_stale_caches(current: &Path) {
const MAX_AGE: std::time::Duration = std::time::Duration::from_secs(60 * 60);
let Some(root) = current.parent() else { return };
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path == current {
continue;
}
let stale = entry
.metadata()
.and_then(|m| m.modified())
.and_then(|m| m.elapsed().map_err(std::io::Error::other))
.is_ok_and(|age| age > MAX_AGE);
if stale {
let _ = std::fs::remove_dir_all(&path);
}
}
}
async fn shared_preinit(stdlib: &Path, imports: &[&str]) -> Arc<Vec<u8>> {
let key = cache_key(imports);
let cache = PREINIT_CACHE
.get_or_init(|| async { Mutex::new(HashMap::new()) })
.await;
let mut cache = cache.lock().await;
if let Some(bytes) = cache.get(&key) {
return Arc::clone(bytes);
}
let dir = cache_dir();
let path = dir.join(format!("{key}.wasm"));
let bytes = if let Ok(bytes) = std::fs::read(&path) {
Arc::new(bytes)
} else {
let bytes = Arc::new(
pre_initialize(stdlib, None, imports, &[])
.await
.expect("pre-initialization should succeed"),
);
std::fs::create_dir_all(&dir).expect("cache directory should be creatable");
prune_stale_caches(&dir);
let tmp = dir.join(format!("{key}.{}.tmp", std::process::id()));
std::fs::write(&tmp, bytes.as_slice()).expect("cache entry should be writable");
std::fs::rename(&tmp, &path).expect("cache entry should be publishable");
bytes
};
cache.insert(key, Arc::clone(&bytes));
bytes
}
#[tokio::test]
async fn preinit_basic() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
assert!(!preinit_bytes.is_empty());
assert_eq!(&preinit_bytes[0..4], b"\0asm");
}
#[tokio::test]
async fn preinit_can_execute() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.expect("sandbox creation should succeed");
let result = sandbox
.execute("print('hello from preinit')")
.await
.expect("execution should succeed");
assert!(result.stdout.contains("hello from preinit"));
}
#[tokio::test]
async fn preinit_arbitrary_imports_work() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.expect("sandbox creation should succeed");
let result = sandbox
.execute(
r#"
import json
import base64
import hashlib
import re
import collections
# Verify they all work
print(f"json: {json.dumps({'a': 1})}")
print(f"base64: {base64.b64encode(b'test').decode()}")
print(f"hashlib: {hashlib.md5(b'test').hexdigest()[:8]}")
print(f"re: {re.match(r'\d+', '123').group()}")
print(f"collections: {type(collections.OrderedDict()).__name__}")
"#,
)
.await
.expect("imports should work");
assert!(result.stdout.contains(r#"json: {"a": 1}"#));
assert!(result.stdout.contains("base64: dGVzdA==")); assert!(result.stdout.contains("hashlib: 098f6bcd")); assert!(result.stdout.contains("re: 123"));
assert!(result.stdout.contains("collections: OrderedDict"));
}
#[tokio::test]
async fn preinit_multiple_sandboxes() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
for i in 0..3 {
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.expect("sandbox creation should succeed");
let result = sandbox
.execute(&format!("print('sandbox {i}')"))
.await
.expect("execution should succeed");
assert!(result.stdout.contains(&format!("sandbox {i}")));
}
}
#[tokio::test]
async fn preinit_sandboxes_isolated() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
let sandbox1 = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.unwrap();
sandbox1
.execute("secret_value = 'sandbox1_secret'")
.await
.unwrap();
let sandbox2 = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.unwrap();
let result = sandbox2
.execute(
r#"
try:
print(f"found: {secret_value}")
except NameError:
print("variable not found - correctly isolated")
"#,
)
.await
.unwrap();
assert!(result.stdout.contains("correctly isolated"));
}
#[tokio::test]
async fn preinit_with_imports() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &["json"]).await;
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.expect("sandbox creation should succeed");
let result = sandbox
.execute(
r#"
import sys
if 'json' in sys.modules:
print("json was pre-imported")
else:
print("json not in sys.modules")
# Should still work
import json
print(json.dumps([1, 2, 3]))
"#,
)
.await
.expect("execution should succeed");
assert!(result.stdout.contains("json was pre-imported"));
assert!(result.stdout.contains("[1, 2, 3]"));
}
#[tokio::test]
async fn preinit_imports_work_within_execution() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.unwrap();
let result = sandbox
.execute(
r#"
import json
import hashlib
print(json.dumps({'works': True}))
print(hashlib.md5(b'test').hexdigest()[:8])
"#,
)
.await
.unwrap();
assert!(result.stdout.contains(r#"{"works": true}"#));
assert!(result.stdout.contains("098f6bcd"));
}
#[tokio::test]
async fn preinit_file_operations_work() {
let stdlib = get_stdlib_path();
let preinit_bytes = shared_preinit(&stdlib, &[]).await;
let sandbox = Sandbox::builder()
.with_wasm_bytes((*preinit_bytes).clone())
.with_python_stdlib(&stdlib)
.build()
.unwrap();
let result = sandbox
.execute(
r#"
import os
# List contents of stdlib (should have some .py files)
files = os.listdir('/python-stdlib')
py_files = [f for f in files if f.endswith('.py') or not '.' in f]
print(f"found {len(py_files)} items")
print(f"has_encodings: {'encodings' in files}")
"#,
)
.await
.unwrap();
assert!(result.stdout.contains("has_encodings: True"));
}