runmat_runtime/testing/
services.rs1use std::cell::RefCell;
2use std::future::Future;
3use std::pin::Pin;
4use std::rc::Rc;
5
6use runmat_value::Value;
7
8use crate::BuiltinResult;
9
10pub type RunSuiteFuture = Pin<Box<dyn Future<Output = BuiltinResult<Value>>>>;
11pub type RunSuiteService = dyn Fn(Value, Vec<Value>) -> RunSuiteFuture;
12pub type RunTestsService = dyn Fn(Vec<Value>) -> RunSuiteFuture;
13pub type DiscoverTestsService = dyn Fn(Vec<Value>) -> RunSuiteFuture;
14
15#[derive(Clone)]
20pub struct RuntimeTestServices {
21 run_suite: Rc<RunSuiteService>,
22 run_tests: Rc<RunTestsService>,
23 discover_tests: Rc<DiscoverTestsService>,
24}
25
26impl std::fmt::Debug for RuntimeTestServices {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 formatter
29 .debug_struct("RuntimeTestServices")
30 .finish_non_exhaustive()
31 }
32}
33
34impl RuntimeTestServices {
35 pub fn new(
36 run_suite: impl Fn(Value, Vec<Value>) -> RunSuiteFuture + 'static,
37 run_tests: impl Fn(Vec<Value>) -> RunSuiteFuture + 'static,
38 discover_tests: impl Fn(Vec<Value>) -> RunSuiteFuture + 'static,
39 ) -> Self {
40 Self {
41 run_suite: Rc::new(run_suite),
42 run_tests: Rc::new(run_tests),
43 discover_tests: Rc::new(discover_tests),
44 }
45 }
46}
47
48thread_local! {
49 static TEST_SERVICES_STACK: RefCell<Vec<RuntimeTestServices>> =
50 const { RefCell::new(Vec::new()) };
51}
52
53pub struct TestServicesGuard {
54 context: Option<Rc<crate::context::RuntimeContextState>>,
55}
56
57impl Drop for TestServicesGuard {
58 fn drop(&mut self) {
59 if let Some(context) = &self.context {
60 let popped = context.test_services.borrow_mut().pop();
61 debug_assert!(popped.is_some());
62 } else {
63 TEST_SERVICES_STACK.with(|stack| {
64 let popped = stack.borrow_mut().pop();
65 debug_assert!(popped.is_some());
66 });
67 }
68 }
69}
70
71pub fn install_test_services(services: RuntimeTestServices) -> TestServicesGuard {
72 let context = crate::context::legacy::active().map(|context| Rc::clone(context.state()));
73 if let Some(context) = &context {
74 context.test_services.borrow_mut().push(services);
75 } else {
76 TEST_SERVICES_STACK.with(|stack| stack.borrow_mut().push(services));
77 }
78 TestServicesGuard { context }
79}
80
81fn current_test_services() -> Option<RuntimeTestServices> {
82 if let Some(context) = crate::context::legacy::active() {
83 return context.state().test_services.borrow().last().cloned();
84 }
85 TEST_SERVICES_STACK.with(|stack| stack.borrow().last().cloned())
86}
87
88pub async fn run_test_suite(suite: Value, plugins: Vec<Value>) -> BuiltinResult<Value> {
89 let service = current_test_services();
90 let Some(service) = service else {
91 return Err(crate::build_runtime_error(
92 "TestRunner.run requires an active Core test executor",
93 )
94 .with_identifier("RunMat:Testing:RequiresExecutor")
95 .with_builtin("matlab.unittest.TestRunner.run")
96 .build());
97 };
98 (service.run_suite)(suite, plugins).await
99}
100
101pub async fn run_tests(args: Vec<Value>) -> BuiltinResult<Value> {
102 let service = current_test_services();
103 let Some(service) = service else {
104 return Err(
105 crate::build_runtime_error("runtests requires an active Core test executor")
106 .with_identifier("RunMat:Testing:RequiresExecutor")
107 .with_builtin("runtests")
108 .build(),
109 );
110 };
111 (service.run_tests)(args).await
112}
113
114pub async fn discover_tests(args: Vec<Value>) -> BuiltinResult<Value> {
115 let service = current_test_services();
116 let Some(service) = service else {
117 return Err(
118 crate::build_runtime_error("testsuite requires an active Core test executor")
119 .with_identifier("RunMat:Testing:RequiresExecutor")
120 .with_builtin("testsuite")
121 .build(),
122 );
123 };
124 (service.discover_tests)(args).await
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn service_installation_is_scoped_and_async_safe() {
133 let _guard = install_test_services(RuntimeTestServices::new(
134 |suite, _plugins| Box::pin(async move { Ok(suite) }),
135 |args| {
136 Box::pin(async move {
137 let count = args.len();
138 Ok(Value::Cell(
139 runmat_value::CellArray::new(args, 1, count).unwrap(),
140 ))
141 })
142 },
143 |args| {
144 Box::pin(async move {
145 Ok(args
146 .into_iter()
147 .next()
148 .unwrap_or_else(|| Value::OutputList(Vec::new())))
149 })
150 },
151 ));
152 let suite = Value::String("suite".into());
153 assert_eq!(
154 futures::executor::block_on(run_test_suite(suite.clone(), Vec::new())).unwrap(),
155 suite
156 );
157 }
158}