Skip to main content

arcane_engine/scripting/
test_runner.rs

1use std::cell::RefCell;
2use std::path::Path;
3use std::rc::Rc;
4
5use anyhow::Context;
6use deno_core::JsRuntime;
7use deno_core::OpState;
8use deno_core::RuntimeOptions;
9
10use super::TsModuleLoader;
11
12/// Result of a single test case.
13#[derive(Debug, Clone)]
14pub struct TestResult {
15    pub suite: String,
16    pub name: String,
17    pub passed: bool,
18    pub error: Option<String>,
19}
20
21/// Summary of a test file run.
22#[derive(Debug, Clone, Default)]
23pub struct TestSummary {
24    pub total: usize,
25    pub passed: usize,
26    pub failed: usize,
27}
28
29// Shared state between the op and the test runner.
30struct TestRunnerState {
31    summary: TestSummary,
32    results: Vec<TestResult>,
33}
34
35/// Run a single `.test.ts` file in V8 and collect results.
36pub fn run_test_file(path: &Path) -> anyhow::Result<TestSummary> {
37    let rt = tokio::runtime::Builder::new_current_thread()
38        .enable_all()
39        .build()?;
40
41    rt.block_on(run_test_file_async(path))
42}
43
44#[deno_core::op2(fast)]
45fn op_report_test(
46    state: &mut OpState,
47    #[string] suite: &str,
48    #[string] name: &str,
49    passed: bool,
50    #[string] error: &str,
51) {
52    let runner_state = state.borrow_mut::<Rc<RefCell<TestRunnerState>>>();
53    let mut s = runner_state.borrow_mut();
54
55    s.results.push(TestResult {
56        suite: suite.to_string(),
57        name: name.to_string(),
58        passed,
59        error: if error.is_empty() {
60            None
61        } else {
62            Some(error.to_string())
63        },
64    });
65
66    if passed {
67        s.summary.passed += 1;
68    } else {
69        s.summary.failed += 1;
70    }
71    s.summary.total += 1;
72}
73
74#[deno_core::op2]
75#[string]
76fn op_crypto_random_uuid_test() -> String {
77    super::runtime::generate_uuid()
78}
79
80deno_core::extension!(
81    test_runner_ext,
82    ops = [op_report_test, op_crypto_random_uuid_test],
83);
84
85async fn run_test_file_async(path: &Path) -> anyhow::Result<TestSummary> {
86    let state = Rc::new(RefCell::new(TestRunnerState {
87        summary: TestSummary::default(),
88        results: Vec::new(),
89    }));
90
91    let mut runtime = JsRuntime::new(RuntimeOptions {
92        module_loader: Some(Rc::new(TsModuleLoader::new())),
93        extensions: vec![test_runner_ext::init()],
94        ..Default::default()
95    });
96
97    // Store our state in the op_state so ops can access it
98    {
99        let op_state = runtime.op_state();
100        op_state.borrow_mut().put(state.clone());
101    }
102
103    // Install polyfills and test reporter
104    runtime.execute_script(
105        "<test_init>",
106        r#"
107        if (typeof globalThis.crypto === "undefined") {
108            globalThis.crypto = {};
109        }
110        if (typeof globalThis.crypto.randomUUID !== "function") {
111            globalThis.crypto.randomUUID = () => Deno.core.ops.op_crypto_random_uuid_test();
112        }
113        globalThis.__reportTest = (suite, name, passed, error) => {
114            Deno.core.ops.op_report_test(suite, name, passed, error ?? "");
115        };
116        "#,
117    ).map_err(|e| anyhow::anyhow!("{e}"))?;
118
119    // Load and execute the test file (this registers describe/it blocks)
120    let abs_path = std::fs::canonicalize(path)
121        .with_context(|| format!("Cannot resolve path: {}", path.display()))?;
122
123    let specifier =
124        deno_core::ModuleSpecifier::from_file_path(&abs_path).map_err(|_| {
125            anyhow::anyhow!("Cannot convert path to specifier: {}", abs_path.display())
126        })?;
127
128    let mod_id = runtime
129        .load_main_es_module(&specifier)
130        .await
131        .with_context(|| format!("Failed to load {}", path.display()))?;
132
133    let eval_result = runtime.mod_evaluate(mod_id);
134    runtime
135        .run_event_loop(Default::default())
136        .await
137        .map_err(|e| anyhow::anyhow!("{e}"))?;
138    eval_result
139        .await
140        .map_err(|e| anyhow::anyhow!("{e}"))?;
141
142    // Run the collected tests
143    let promise = runtime.execute_script(
144        "<run_tests>",
145        "(async () => { await globalThis.__runTests(); })()",
146    ).map_err(|e| anyhow::anyhow!("{e}"))?;
147
148    let resolve = runtime.resolve(promise);
149    runtime
150        .run_event_loop(Default::default())
151        .await
152        .map_err(|e| anyhow::anyhow!("{e}"))?;
153    resolve.await.map_err(|e| anyhow::anyhow!("{e}"))?;
154
155    // Retrieve results from shared state
156    let summary = state.borrow().summary.clone();
157    Ok(summary)
158}