#![cfg(not(target_arch = "wasm32"))]
use runmat_builtins::{NumericDType, Tensor, Value};
use runmat_filesystem::{self as vfs, SandboxFsProvider};
#[cfg(not(target_arch = "wasm32"))]
use runmat_filesystem::{RemoteFsConfig, RemoteFsProvider};
use runmat_runtime::builtins::io::repl_fs::REPL_FS_TEST_LOCK;
use runmat_runtime::call_builtin;
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use tempfile::tempdir;
#[test]
fn sandbox_provider_supports_repl_and_tabular_builtins() {
let _lock = REPL_FS_TEST_LOCK.lock().unwrap();
let temp = tempdir().expect("temp dir");
let sandbox = Arc::new(
SandboxFsProvider::new(temp.path().to_path_buf()).expect("sandbox provider must init"),
);
let _guard = vfs::replace_provider(sandbox);
call_builtin("mkdir", &[Value::from("reports")]).expect("mkdir succeeds");
let matrix = Tensor {
data: vec![1.0, 2.0, 3.5, 4.0],
shape: vec![2, 2],
rows: 2,
cols: 2,
dtype: NumericDType::F64,
};
let filename = Value::from("reports/data.csv".to_string());
call_builtin(
"dlmwrite",
&[filename.clone(), Value::Tensor(matrix.clone())],
)
.expect("dlmwrite succeeds");
let host_path = temp.path().join("reports").join("data.csv");
assert!(
host_path.exists(),
"backing filesystem should observe sandbox writes"
);
let read_back = call_builtin("dlmread", &[filename]).expect("dlmread succeeds");
assert_eq!(read_back, Value::Tensor(matrix));
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn remote_provider_supports_repl_and_tabular_builtins() {
let _lock = REPL_FS_TEST_LOCK.lock().unwrap();
let server = remote_test_support::RemoteTestServer::spawn();
let remote = Arc::new(
RemoteFsProvider::new(RemoteFsConfig {
base_url: server.base_url(),
auth_token: None,
chunk_bytes: 1_024,
parallel_requests: 2,
direct_read_threshold_bytes: 64 * 1024 * 1024,
timeout: Duration::from_secs(30),
..RemoteFsConfig::default()
})
.expect("remote provider init"),
);
let _guard = vfs::replace_provider(remote);
call_builtin("mkdir", &[Value::from("remote")]).expect("mkdir succeeds");
let matrix = Tensor {
data: vec![10.0, 20.0, 30.0, 40.0],
shape: vec![2, 2],
rows: 2,
cols: 2,
dtype: NumericDType::F64,
};
let filename = Value::from("remote/data.csv".to_string());
call_builtin(
"dlmwrite",
&[filename.clone(), Value::Tensor(matrix.clone())],
)
.expect("dlmwrite succeeds");
let roundtrip = call_builtin("dlmread", &[filename]).expect("dlmread succeeds");
assert_eq!(roundtrip, Value::Tensor(matrix));
server.shutdown();
}
#[cfg(not(target_arch = "wasm32"))]
mod remote_test_support {
use super::*;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, post, put};
use axum::{Json, Router};
use serde::Deserialize;
use std::net::TcpListener as StdTcpListener;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::net::TcpListener as TokioTcpListener;
use tokio::runtime::Runtime;
pub(super) struct RemoteTestServer {
addr: String,
_temp: TempDir,
runtime: Runtime,
}
impl RemoteTestServer {
pub fn spawn() -> Self {
let temp = tempdir().expect("tempdir");
let harness = Harness::new(temp.path().to_path_buf());
let router = Router::new()
.route("/fs/metadata", get(metadata_handler))
.route("/fs/read", get(read_handler))
.route("/fs/write", put(write_handler))
.route("/fs/mkdir", post(mkdir_handler))
.route("/fs/file", delete(delete_file_handler))
.with_state(harness);
let std_listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
std_listener.set_nonblocking(true).expect("nonblocking");
let addr = std_listener.local_addr().unwrap();
let runtime = Runtime::new().expect("runtime");
runtime.spawn(async move {
let listener = TokioTcpListener::from_std(std_listener).unwrap();
axum::serve(listener, router.into_make_service())
.await
.unwrap();
});
Self {
addr: format!("http://{}", addr),
_temp: temp,
runtime,
}
}
pub fn base_url(&self) -> String {
self.addr.clone()
}
pub fn shutdown(self) {
self.runtime.shutdown_background();
}
}
#[derive(Clone)]
struct Harness {
root: Arc<PathBuf>,
}
impl Harness {
fn new(root: PathBuf) -> Self {
Self {
root: Arc::new(root),
}
}
fn resolve(&self, remote: &str) -> PathBuf {
let trimmed = remote.trim_start_matches('/');
self.root.join(trimmed)
}
}
#[derive(Deserialize)]
struct PathParams {
path: String,
offset: Option<u64>,
length: Option<usize>,
truncate: Option<String>,
}
#[derive(Deserialize)]
struct MkdirRequest {
path: String,
recursive: bool,
}
async fn metadata_handler(
State(harness): State<Harness>,
Query(params): Query<PathParams>,
) -> Json<serde_json::Value> {
let stats = std::fs::metadata(harness.resolve(¶ms.path)).unwrap();
Json(serde_json::json!({
"fileType": if stats.is_dir() { "dir" } else { "file" },
"len": stats.len(),
"modified": stats.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_secs()),
"readonly": stats.permissions().readonly()
}))
}
async fn read_handler(
State(harness): State<Harness>,
Query(params): Query<PathParams>,
) -> (axum::http::HeaderMap, Vec<u8>) {
let mut data = std::fs::read(harness.resolve(¶ms.path)).unwrap();
let offset = params.offset.unwrap_or(0) as usize;
let length = params.length.unwrap_or(data.len().saturating_sub(offset));
let end = std::cmp::min(offset + length, data.len());
data = data[offset..end].to_vec();
(axum::http::HeaderMap::new(), data)
}
async fn write_handler(
State(harness): State<Harness>,
Query(params): Query<PathParams>,
body: axum::body::Bytes,
) -> StatusCode {
let target = harness.resolve(¶ms.path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let truncate = params.truncate.as_deref() == Some("true");
let mut data = if truncate || !target.exists() {
Vec::new()
} else {
std::fs::read(&target).unwrap()
};
let offset = params.offset.unwrap_or(0) as usize;
let required = offset + body.len();
if required > data.len() {
data.resize(required, 0);
}
data[offset..offset + body.len()].copy_from_slice(&body);
std::fs::write(target, data).unwrap();
StatusCode::NO_CONTENT
}
async fn mkdir_handler(
State(harness): State<Harness>,
Json(payload): Json<MkdirRequest>,
) -> StatusCode {
let target = harness.resolve(&payload.path);
if payload.recursive {
std::fs::create_dir_all(target).unwrap();
} else {
std::fs::create_dir(target).unwrap();
}
StatusCode::NO_CONTENT
}
async fn delete_file_handler(
State(harness): State<Harness>,
Query(params): Query<PathParams>,
) -> StatusCode {
let target = harness.resolve(¶ms.path);
let _ = std::fs::remove_file(target);
StatusCode::NO_CONTENT
}
}