#![allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(not(feature = "embedded"))]
use std::path::PathBuf;
use std::time::Duration;
use eryx::{Error, ResourceLimits, Sandbox};
#[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")
}
fn sandbox_builder() -> eryx::SandboxBuilder<eryx::state::Has, eryx::state::Has> {
#[cfg(feature = "embedded")]
{
Sandbox::embedded()
}
#[cfg(not(feature = "embedded"))]
{
let stdlib_path = python_stdlib_path();
Sandbox::builder()
.with_wasm_file(runtime_wasm_path())
.with_python_stdlib(&stdlib_path)
}
}
fn sandbox_builder_with_short_timeout() -> eryx::SandboxBuilder<eryx::state::Has, eryx::state::Has>
{
sandbox_builder().with_resource_limits(ResourceLimits {
execution_timeout: Some(Duration::from_secs(3)),
..Default::default()
})
}
#[tokio::test]
async fn test_execute_cancellable_completes_normally() {
let sandbox = sandbox_builder().build().expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("print('Hello from cancellable!')");
let result = handle.wait().await;
assert!(result.is_ok(), "Should complete normally: {:?}", result);
let output = result.unwrap();
assert!(output.stdout.contains("Hello from cancellable!"));
}
#[tokio::test]
async fn test_execute_cancellable_cancel_infinite_loop() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("while True: pass");
let cancel_handle = handle.cancellation_token();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
cancel_handle.cancel();
});
let result = handle.wait().await;
assert!(result.is_err(), "Should be cancelled");
match result {
Err(Error::Cancelled) => {} Err(e) => panic!("Expected Cancelled error, got: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}
#[tokio::test]
async fn test_execute_cancellable_cancel_immediately() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable(
r#"
import time
for i in range(1000):
time.sleep(0.01)
print(f"Iteration {i}")
"#,
);
handle.cancel();
let result = handle.wait().await;
assert!(result.is_err(), "Should be cancelled");
match result {
Err(Error::Cancelled) => {} Err(e) => panic!("Expected Cancelled error, got: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}
#[tokio::test]
async fn test_is_running_before_and_after_completion() {
let sandbox = sandbox_builder().build().expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("x = 1 + 1");
let _ = handle.is_running();
let result = handle.wait().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_is_running_after_cancel() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("while True: pass");
handle.cancel();
assert!(!handle.is_running(), "Should not be running after cancel");
let _ = handle.wait().await;
}
#[tokio::test]
async fn test_cancel_multiple_times_is_idempotent() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("while True: pass");
handle.cancel();
handle.cancel();
handle.cancel();
let result = handle.wait().await;
assert!(result.is_err(), "Should be cancelled");
match result {
Err(Error::Cancelled) => {} Err(e) => panic!("Expected Cancelled error, got: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}
#[tokio::test]
async fn test_cancellation_token_can_be_shared() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("while True: pass");
let token = handle.cancellation_token();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let result = handle.wait().await;
assert!(result.is_err(), "Should be cancelled via shared token");
match result {
Err(Error::Cancelled) => {} Err(e) => panic!("Expected Cancelled error, got: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}
#[tokio::test]
async fn test_cancel_during_python_computation() {
let sandbox = sandbox_builder_with_short_timeout()
.build()
.expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable(
r#"
result = 0
for i in range(10_000_000):
result += i
print(result)
"#,
);
let token = handle.cancellation_token();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
token.cancel();
});
let result = handle.wait().await;
match result {
Ok(_) => {} Err(Error::Cancelled) => {} Err(e) => panic!("Unexpected error: {:?}", e),
}
}
#[tokio::test]
async fn test_fast_execution_completes_before_cancel() {
let sandbox = sandbox_builder().build().expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("print('fast')");
let token = handle.cancellation_token();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
token.cancel();
});
let result = handle.wait().await;
assert!(
result.is_ok(),
"Fast execution should complete: {:?}",
result
);
let output = result.unwrap();
assert!(output.stdout.contains("fast"));
}
#[tokio::test]
async fn test_python_error_not_confused_with_cancellation() {
let sandbox = sandbox_builder().build().expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("raise ValueError('test error')");
let result = handle.wait().await;
assert!(result.is_err(), "Should fail with Python error");
match result {
Err(Error::Cancelled) => panic!("Should not be Cancelled error"),
Err(Error::Execution(msg)) => {
assert!(
msg.contains("ValueError") || msg.contains("test error"),
"Error should mention ValueError: {}",
msg
);
}
Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}
#[tokio::test]
async fn test_syntax_error_not_confused_with_cancellation() {
let sandbox = sandbox_builder().build().expect("Failed to build sandbox");
let handle = sandbox.execute_cancellable("def broken(");
let result = handle.wait().await;
assert!(result.is_err(), "Should fail with syntax error");
match result {
Err(Error::Cancelled) => panic!("Should not be Cancelled error"),
Err(Error::Execution(_)) => {} Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected error, got success"),
}
}