#![allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(not(feature = "embedded"))]
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use eryx::{PythonExecutor, PythonStateSnapshot, SessionExecutor};
static SHARED_EXECUTOR: OnceLock<Arc<PythonExecutor>> = OnceLock::new();
fn get_shared_executor() -> Arc<PythonExecutor> {
SHARED_EXECUTOR
.get_or_init(|| Arc::new(create_executor()))
.clone()
}
fn create_executor() -> PythonExecutor {
#[cfg(feature = "embedded")]
{
use eryx::embedded::EmbeddedResources;
let resources = EmbeddedResources::get().expect("Failed to extract embedded resources");
#[allow(unsafe_code)]
return unsafe {
PythonExecutor::from_precompiled_file(&resources.runtime_path)
.expect("Failed to load embedded runtime")
.with_python_stdlib(&resources.stdlib_path)
};
}
#[cfg(not(feature = "embedded"))]
{
let stdlib_path = python_stdlib_path();
let path = runtime_wasm_path();
PythonExecutor::from_file(&path)
.unwrap_or_else(|e| panic!("Failed to load runtime.wasm from {:?}: {}", path, e))
.with_python_stdlib(&stdlib_path)
}
}
#[cfg(not(feature = "embedded"))]
fn runtime_wasm_path() -> PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
PathBuf::from(manifest_dir)
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("eryx-runtime")
.join("runtime.wasm")
}
#[cfg(not(feature = "embedded"))]
fn python_stdlib_path() -> PathBuf {
if let Ok(path) = std::env::var("ERYX_PYTHON_STDLIB") {
let path = PathBuf::from(path);
if path.exists() {
return path;
}
}
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
PathBuf::from(manifest_dir)
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("eryx-wasm-runtime")
.join("tests")
.join("python-stdlib")
}
async fn create_session() -> SessionExecutor {
let executor = get_shared_executor();
SessionExecutor::new(executor, &[])
.await
.unwrap_or_else(|e| panic!("Failed to create session: {}", e))
}
#[tokio::test]
async fn test_variable_persistence() {
let mut session = create_session().await;
let output = session
.execute("x = 42")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to execute x = 42: {}", e));
assert_eq!(output.stdout, "", "Assignment should produce no output");
let output = session
.execute("print(x)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to execute print(x): {}", e));
assert_eq!(
output.stdout, "42",
"Variable x should persist and equal 42"
);
let output = session
.execute("x = x + 1")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to execute x = x + 1: {}", e));
assert_eq!(output.stdout, "", "Assignment should produce no output");
let output = session
.execute("print(x)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to execute print(x) after modification: {}", e));
assert_eq!(
output.stdout, "43",
"Variable x should be 43 after increment"
);
}
#[tokio::test]
async fn test_function_persistence() {
let mut session = create_session().await;
let output = session
.execute(
r#"
def greet(name):
return f"Hello, {name}!"
"#,
)
.run()
.await
.unwrap_or_else(|e| panic!("Failed to define function: {}", e));
assert_eq!(
output.stdout, "",
"Function definition should produce no output"
);
let output = session
.execute("print(greet('World'))")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call greet function: {}", e));
assert_eq!(
output.stdout, "Hello, World!",
"Function should be callable"
);
}
#[tokio::test]
async fn test_class_persistence() {
let mut session = create_session().await;
let output = session
.execute(
r#"
class MyCounter:
def __init__(self, start=0):
self.value = start
def increment(self):
self.value += 1
return self.value
"#,
)
.run()
.await
.unwrap_or_else(|e| panic!("Failed to define class: {}", e));
assert_eq!(
output.stdout, "",
"Class definition should produce no output"
);
let output = session
.execute("counter = MyCounter(10)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to create instance: {}", e));
assert_eq!(
output.stdout, "",
"Instance creation should produce no output"
);
let output = session
.execute("print(counter.increment())")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call increment: {}", e));
assert_eq!(output.stdout, "11", "First increment should return 11");
let output = session
.execute("print(counter.increment())")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call second increment: {}", e));
assert_eq!(output.stdout, "12", "Second increment should return 12");
}
#[tokio::test]
async fn test_clear_state() {
let mut session = create_session().await;
session
.execute("x = 100")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set x: {}", e));
let output = session
.execute("print(x)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to print x: {}", e));
assert_eq!(output.stdout, "100");
session
.clear_state()
.await
.unwrap_or_else(|e| panic!("Failed to clear state: {}", e));
let result = session.execute("print(x)").run().await;
assert!(
result.is_err(),
"After clear_state, x should not be defined: {:?}",
result
);
}
#[tokio::test]
async fn test_reset_clears_state() {
let mut session = create_session().await;
session
.execute("x = 100")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set x: {}", e));
let output = session
.execute("print(x)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to print x: {}", e));
assert_eq!(output.stdout, "100");
session
.reset(&[])
.await
.unwrap_or_else(|e| panic!("Failed to reset session: {}", e));
let result = session.execute("print(x)").run().await;
assert!(
result.is_err(),
"After reset, x should not be defined: {:?}",
result
);
}
#[tokio::test]
async fn test_complex_state_persistence() {
let mut session = create_session().await;
session
.execute("data = []")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to create list: {}", e));
session
.execute("data.append(1)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to append 1: {}", e));
session
.execute("data.append(2)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to append 2: {}", e));
session
.execute("data.append(3)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to append 3: {}", e));
let output = session
.execute("print(sum(data))")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to sum data: {}", e));
assert_eq!(output.stdout, "6", "Sum of [1, 2, 3] should be 6");
let output = session
.execute("print(len(data))")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to get len: {}", e));
assert_eq!(output.stdout, "3", "Length should be 3");
}
#[tokio::test]
async fn test_execution_count() {
let mut session = create_session().await;
assert_eq!(session.execution_count(), 0, "Initial count should be 0");
session
.execute("x = 1")
.run()
.await
.unwrap_or_else(|e| panic!("exec 1 failed: {}", e));
assert_eq!(session.execution_count(), 1);
session
.execute("x = 2")
.run()
.await
.unwrap_or_else(|e| panic!("exec 2 failed: {}", e));
assert_eq!(session.execution_count(), 2);
session
.execute("x = 3")
.run()
.await
.unwrap_or_else(|e| panic!("exec 3 failed: {}", e));
assert_eq!(session.execution_count(), 3);
session
.reset(&[])
.await
.unwrap_or_else(|e| panic!("reset failed: {}", e));
assert_eq!(
session.execution_count(),
0,
"Count should be 0 after reset"
);
}
#[tokio::test]
async fn test_snapshot_and_restore() {
let mut session = create_session().await;
session
.execute("x = 10")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set x: {}", e));
session
.execute("y = 20")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set y: {}", e));
session
.execute("data = [1, 2, 3]")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set data: {}", e));
let snapshot = session
.snapshot_state()
.await
.unwrap_or_else(|e| panic!("Failed to snapshot: {}", e));
assert!(snapshot.size() > 0, "Snapshot should have data");
session
.execute("x = 999")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to modify x: {}", e));
let output = session
.execute("print(x)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to print x: {}", e));
assert_eq!(output.stdout, "999");
session
.restore_state(&snapshot)
.await
.unwrap_or_else(|e| panic!("Failed to restore: {}", e));
let output = session
.execute("print(x)")
.run()
.await
.expect("Failed to read x");
assert_eq!(output.stdout, "10", "x should be restored to 10");
let output = session
.execute("print(y)")
.run()
.await
.expect("Failed to read y");
assert_eq!(output.stdout, "20", "y should be restored to 20");
let output = session
.execute("print(data)")
.run()
.await
.expect("Failed to read data");
assert_eq!(output.stdout, "[1, 2, 3]", "data should be restored");
}
#[tokio::test]
async fn test_snapshot_serialization() {
let mut session = create_session().await;
session
.execute("value = 42")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set value: {}", e));
let snapshot = session
.snapshot_state()
.await
.unwrap_or_else(|e| panic!("Failed to snapshot: {}", e));
let bytes = snapshot.to_bytes();
assert!(
bytes.len() > 8,
"Serialized bytes should include header + data"
);
let restored_snapshot = PythonStateSnapshot::from_bytes(&bytes)
.unwrap_or_else(|e| panic!("Failed to deserialize: {}", e));
assert_eq!(restored_snapshot.size(), snapshot.size());
assert_eq!(
restored_snapshot.metadata().timestamp_ms,
snapshot.metadata().timestamp_ms
);
session
.clear_state()
.await
.unwrap_or_else(|e| panic!("Failed to clear: {}", e));
session
.restore_state(&restored_snapshot)
.await
.unwrap_or_else(|e| panic!("Failed to restore from deserialized: {}", e));
let output = session
.execute("print(value)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to print value: {}", e));
assert_eq!(output.stdout, "42");
}
#[tokio::test]
async fn test_snapshot_with_unserializable() {
let mut session = create_session().await;
session
.execute("import sys")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to import sys: {}", e));
session
.execute("num = 100")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to set num: {}", e));
let snapshot = session
.snapshot_state()
.await
.unwrap_or_else(|e| panic!("Failed to snapshot: {}", e));
session.clear_state().await.unwrap();
session.restore_state(&snapshot).await.unwrap();
let output = session
.execute("print(num)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to print num: {}", e));
assert_eq!(output.stdout, "100", "num should be restored");
}
#[tokio::test]
async fn test_snapshot_with_functions() {
let mut session = create_session().await;
session
.execute("def greet(name): return f'Hello, {name}!'")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to define function: {}", e));
session
.execute("fn = lambda x: x * 2")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to define lambda: {}", e));
let snapshot = session
.snapshot_state()
.await
.unwrap_or_else(|e| panic!("Failed to snapshot: {}", e));
assert!(
snapshot.size() > 10,
"Snapshot should contain serialized data"
);
session.clear_state().await.unwrap();
session
.restore_state(&snapshot)
.await
.unwrap_or_else(|e| panic!("Failed to restore: {}", e));
let output = session
.execute("print(greet('World'))")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call greet: {}", e));
assert_eq!(output.stdout, "Hello, World!");
let output = session
.execute("print(fn(21))")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call lambda: {}", e));
assert_eq!(output.stdout, "42");
}
#[tokio::test]
async fn test_snapshot_with_classes() {
let mut session = create_session().await;
session
.execute(
r#"
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self):
return (self.x**2 + self.y**2) ** 0.5
"#,
)
.run()
.await
.unwrap_or_else(|e| panic!("Failed to define class: {}", e));
session
.execute("p = Point(3, 4)")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to create instance: {}", e));
let snapshot = session
.snapshot_state()
.await
.unwrap_or_else(|e| panic!("Failed to snapshot: {}", e));
session.clear_state().await.unwrap();
session
.restore_state(&snapshot)
.await
.unwrap_or_else(|e| panic!("Failed to restore: {}", e));
let output = session
.execute("print(p.distance())")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to call method: {}", e));
assert_eq!(output.stdout, "5.0");
let output = session
.execute("print(Point(5, 12).distance())")
.run()
.await
.unwrap_or_else(|e| panic!("Failed to create new instance: {}", e));
assert_eq!(output.stdout, "13.0");
}