Skip to main content

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