arcane_engine/scripting/
test_runner.rs1use 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#[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#[derive(Debug, Clone, Default)]
23pub struct TestSummary {
24 pub total: usize,
25 pub passed: usize,
26 pub failed: usize,
27}
28
29struct TestRunnerState {
31 summary: TestSummary,
32 results: Vec<TestResult>,
33}
34
35pub fn run_test_file(path: &Path) -> anyhow::Result<TestSummary> {
37 run_test_file_with_import_map(path, ImportMap::new())
38}
39
40pub 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 {
104 let op_state = runtime.op_state();
105 op_state.borrow_mut().put(state.clone());
106 }
107
108 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 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 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 let summary = state.borrow().summary.clone();
162 Ok(summary)
163}