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