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::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 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 {
99 let op_state = runtime.op_state();
100 op_state.borrow_mut().put(state.clone());
101 }
102
103 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 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 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 let summary = state.borrow().summary.clone();
157 Ok(summary)
158}