Skip to main content

runmat_runtime/testing/
services.rs

1use 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/// Runtime-facing ports supplied by the active Core execution composition.
16///
17/// MATLAB builtins may project arguments and results, but all lifecycle and
18/// scheduling authority remains behind these services.
19#[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}