#![cfg(not(target_arch = "wasm32"))]
use anyhow::Result;
use runmat_builtins::Value;
use runmat_core::{InputRequest, InputRequestKind, InputResponse, RunError, RunMatSession};
use runmat_runtime::interaction::force_interactive_stdin_for_tests;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, OnceLock};
static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn test_mutex() -> &'static Mutex<()> {
TEST_MUTEX.get_or_init(|| Mutex::new(()))
}
struct InteractiveGuard;
impl InteractiveGuard {
fn new() -> Self {
force_interactive_stdin_for_tests(true);
Self
}
}
impl Drop for InteractiveGuard {
fn drop(&mut self) {
force_interactive_stdin_for_tests(false);
}
}
fn value_as_f64(value: &Value) -> Option<f64> {
match value {
Value::Num(n) => Some(*n),
_ => None,
}
}
fn value_as_char_row(value: &Value) -> Option<String> {
match value {
Value::CharArray(ca) if ca.rows == 1 => Some(ca.data.iter().collect()),
_ => None,
}
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn input_prompts_return_value() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let prompts = Arc::new(Mutex::new(Vec::new()));
let prompts_clone = Arc::clone(&prompts);
session.install_async_input_handler(move |request: InputRequest| {
let prompts_clone = Arc::clone(&prompts_clone);
async move {
prompts_clone.lock().unwrap().push(request.prompt.clone());
Ok(InputResponse::Line("41".into()))
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"value = input('Enter value: '); value = value + 1; value",
)
.map_err(anyhow::Error::new)?;
let value = result.value.expect("execution should produce a value");
assert_eq!(value_as_f64(&value), Some(42.0));
assert_eq!(result.stdin_events.len(), 1);
assert_eq!(prompts.lock().unwrap().as_slice(), &["Enter value: "]);
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn multiple_inputs_call_handler_in_order() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let responses = Arc::new(Mutex::new(VecDeque::from([
InputResponse::Line("5".into()),
InputResponse::Line("code-42".into()),
])));
let responses_clone = Arc::clone(&responses);
session.install_async_input_handler(move |_request: InputRequest| {
let responses_clone = Arc::clone(&responses_clone);
async move {
let response = responses_clone
.lock()
.unwrap()
.pop_front()
.expect("missing queued response");
Ok(response)
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"first = input('First: '); second = input('Second: ', \"s\"); second",
)
.map_err(anyhow::Error::new)?;
let value = result
.value
.expect("final execution should produce a value");
assert_eq!(value_as_char_row(&value), Some("code-42".to_string()));
assert_eq!(result.stdin_events.len(), 2);
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn char_literal_round_trips() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let result = runmat_core::execute_text_request_for_testing(&mut session, "'s'")
.map_err(anyhow::Error::new)?;
let value = result.value.expect("char literal should return a value");
assert_eq!(value_as_char_row(&value), Some("s".to_string()));
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn pause_uses_keypress_handler() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let kinds = Arc::new(Mutex::new(Vec::new()));
let kinds_clone = Arc::clone(&kinds);
session.install_async_input_handler(move |request: InputRequest| {
let kinds_clone = Arc::clone(&kinds_clone);
async move {
kinds_clone.lock().unwrap().push(request.kind.clone());
Ok(InputResponse::KeyPress)
}
});
let result =
runmat_core::execute_text_request_for_testing(&mut session, "pause; value = 1; value")
.map_err(anyhow::Error::new)?;
let value = result.value.expect("execution should produce a value");
assert_eq!(value_as_f64(&value), Some(1.0));
assert_eq!(result.stdin_events.len(), 1);
assert!(matches!(
kinds.lock().unwrap().as_slice(),
[InputRequestKind::KeyPress]
));
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn pending_handler_returns_error() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
session.install_async_input_handler(|_request: InputRequest| async move {
Err("input handler is unavailable".to_string())
});
let result =
runmat_core::execute_text_request_for_testing(&mut session, "pause; value = 1; value");
match result {
Err(RunError::Runtime(err)) => {
assert_eq!(
err.identifier(),
Some("RunMat:interaction:AsyncHandlerError")
);
}
Err(other) => panic!("expected runtime interaction error, got: {other:?}"),
Ok(_exec) => {}
}
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn spawn_of_async_function_triggers_pause_handler_before_await() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let kinds = Arc::new(Mutex::new(Vec::new()));
let kinds_clone = Arc::clone(&kinds);
session.install_async_input_handler(move |request: InputRequest| {
let kinds_clone = Arc::clone(&kinds_clone);
async move {
kinds_clone.lock().unwrap().push(request.kind.clone());
Ok(InputResponse::KeyPress)
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"async function y = wait_for_key(); pause; y = 1; end; \
t = spawn(wait_for_key()); marker = 7;",
)
.map_err(anyhow::Error::new)?;
assert!(
result.error.is_none(),
"spawn of async function with pause handler should not raise runtime errors"
);
assert_eq!(
result.stdin_events.len(),
1,
"spawn should trigger pause interaction before await in current runtime model"
);
assert!(matches!(
kinds.lock().unwrap().as_slice(),
[InputRequestKind::KeyPress]
));
let marker = runmat_core::execute_text_request_for_testing(&mut session, "marker")
.map_err(anyhow::Error::new)?;
let marker_value = marker
.value
.expect("marker readback should produce a value");
assert_eq!(value_as_f64(&marker_value), Some(7.0));
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn parallel_spawn_inputs_follow_spawn_order_not_await_order() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let prompts = Arc::new(Mutex::new(Vec::new()));
let prompts_clone = Arc::clone(&prompts);
let responses = Arc::new(Mutex::new(VecDeque::from([
InputResponse::Line("11".into()),
InputResponse::Line("22".into()),
])));
let responses_clone = Arc::clone(&responses);
session.install_async_input_handler(move |request: InputRequest| {
let prompts_clone = Arc::clone(&prompts_clone);
let responses_clone = Arc::clone(&responses_clone);
async move {
prompts_clone.lock().unwrap().push(request.prompt.clone());
let response = responses_clone
.lock()
.unwrap()
.pop_front()
.expect("missing queued response");
Ok(response)
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"async function y = first(); input('first: '); y = 1; end; \
async function y = second(); input('second: '); y = 2; end; \
t1 = spawn(first()); t2 = spawn(second()); out2 = await(t2); out1 = await(t1);",
)
.map_err(anyhow::Error::new)?;
assert!(
result.error.is_none(),
"parallel spawn/await input flow should not raise runtime errors"
);
assert_eq!(result.stdin_events.len(), 2);
assert_eq!(prompts.lock().unwrap().as_slice(), &["first: ", "second: "]);
let out1 = runmat_core::execute_text_request_for_testing(&mut session, "out1")
.map_err(anyhow::Error::new)?;
let out1_value = out1.value.expect("out1 readback should produce a value");
assert_eq!(value_as_f64(&out1_value), Some(1.0));
let out2 = runmat_core::execute_text_request_for_testing(&mut session, "out2")
.map_err(anyhow::Error::new)?;
let out2_value = out2.value.expect("out2 readback should produce a value");
assert_eq!(value_as_f64(&out2_value), Some(2.0));
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn spawn_error_stops_later_spawn_from_running() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let prompts = Arc::new(Mutex::new(Vec::new()));
let prompts_clone = Arc::clone(&prompts);
session.install_async_input_handler(move |request: InputRequest| {
let prompts_clone = Arc::clone(&prompts_clone);
async move {
prompts_clone.lock().unwrap().push(request.prompt.clone());
Err("handler failed".to_string())
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"async function y = first(); input('first: '); y = 1; end; \
async function y = second(); input('second: '); y = 2; end; \
t1 = spawn(first()); t2 = spawn(second()); marker = 7;",
);
match result {
Err(RunError::Runtime(err)) => {
assert!(
matches!(
err.identifier(),
Some("RunMat:input:InteractionFailed")
| Some("RunMat:interaction:AsyncHandlerError")
),
"unexpected interaction error identifier: {:?}",
err.identifier()
);
}
Err(other) => panic!("expected runtime interaction error, got: {other:?}"),
Ok(_exec) => {}
}
assert_eq!(
prompts.lock().unwrap().as_slice(),
&["first: "],
"second spawn should not run after first spawn interaction failure"
);
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn async_call_without_await_or_spawn_stays_lazy_until_await() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let prompts = Arc::new(Mutex::new(Vec::new()));
let prompts_clone = Arc::clone(&prompts);
session.install_async_input_handler(move |request: InputRequest| {
let prompts_clone = Arc::clone(&prompts_clone);
async move {
prompts_clone.lock().unwrap().push(request.prompt.clone());
Ok(InputResponse::Line("11".into()))
}
});
let result = runmat_core::execute_text_request_for_testing(
&mut session,
"async function y = asks(); input('lazy: '); y = 1; end; \
fut = asks(); marker = 9;",
)
.map_err(anyhow::Error::new)?;
assert!(
result.error.is_none(),
"direct async call should not raise runtime errors in current runtime model"
);
assert_eq!(
result.stdin_events.len(),
0,
"direct async call should remain lazy before await/spawn boundaries"
);
assert!(
prompts.lock().unwrap().is_empty(),
"direct async call should not prompt until awaited or spawned"
);
let marker = runmat_core::execute_text_request_for_testing(&mut session, "marker")
.map_err(anyhow::Error::new)?;
let marker_value = marker
.value
.expect("marker readback should produce a value");
assert_eq!(value_as_f64(&marker_value), Some(9.0));
let fut = runmat_core::execute_text_request_for_testing(&mut session, "out = await(fut);")
.map_err(anyhow::Error::new)?;
assert!(
fut.error.is_none(),
"awaiting deferred future should complete without runtime error"
);
let out = runmat_core::execute_text_request_for_testing(&mut session, "out")
.map_err(anyhow::Error::new)?;
let fut_value = out
.value
.expect("awaited future result should be materialized in out");
assert_eq!(
value_as_f64(&fut_value),
Some(1.0),
"await should resolve the deferred async function call"
);
assert_eq!(
prompts.lock().unwrap().as_slice(),
&["lazy: "],
"await should trigger the deferred interaction once"
);
Ok(())
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn deferred_future_triggers_interaction_when_spawned() -> Result<()> {
let _test_guard = test_mutex()
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let _guard = InteractiveGuard::new();
let mut session = RunMatSession::with_options(false, false)?;
let prompts = Arc::new(Mutex::new(Vec::new()));
let prompts_clone = Arc::clone(&prompts);
session.install_async_input_handler(move |request: InputRequest| {
let prompts_clone = Arc::clone(&prompts_clone);
async move {
prompts_clone.lock().unwrap().push(request.prompt.clone());
Ok(InputResponse::Line("5".into()))
}
});
let deferred = runmat_core::execute_text_request_for_testing(
&mut session,
"async function y = asks(); input('spawned: '); y = 5; end; \
fut = asks(); marker = 3;",
)
.map_err(anyhow::Error::new)?;
assert!(
deferred.error.is_none(),
"deferred async call should compile/execute without runtime errors"
);
assert_eq!(
deferred.stdin_events.len(),
0,
"future creation should remain lazy before spawn/await boundaries"
);
assert!(
prompts.lock().unwrap().is_empty(),
"future creation should not trigger input prompts"
);
let spawned = runmat_core::execute_text_request_for_testing(
&mut session,
"t = spawn(fut); marker_after_spawn = 7;",
)
.map_err(anyhow::Error::new)?;
assert!(
spawned.error.is_none(),
"spawning deferred future should not raise runtime errors"
);
assert_eq!(
spawned.stdin_events.len(),
1,
"spawn should realize deferred future and trigger input interaction"
);
assert_eq!(
prompts.lock().unwrap().as_slice(),
&["spawned: "],
"spawn should forward deferred future prompt through input handler"
);
let marker = runmat_core::execute_text_request_for_testing(&mut session, "marker_after_spawn")
.map_err(anyhow::Error::new)?;
let marker_value = marker
.value
.expect("spawn follow-up marker should be readable");
assert_eq!(value_as_f64(&marker_value), Some(7.0));
Ok(())
}