use crate::engine::ctx::Ctx as EngineCtx;
use crate::engine::mock_server::BridgedRequest;
use crate::engine::{ScenarioInfo, ScenarioResult, ScriptHost, TopLevel};
use rquickjs::loader::{ImportAttributes, Loader, Resolver};
use rquickjs::module::Declared;
use rquickjs::{
AsyncContext, AsyncRuntime, Ctx as JsCtx, Error as JsError, Function, Module, Persistent, Value,
};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
pub type EnvVars = Arc<Mutex<HashMap<String, String>>>;
#[derive(Default)]
pub struct Registry {
scenarios: Mutex<Vec<(ScenarioInfo, Persistent<Function<'static>>)>>,
setup: Mutex<Option<Persistent<Function<'static>>>>,
teardown: Mutex<Option<Persistent<Function<'static>>>>,
pub(super) bridged: Mutex<
Vec<(
mpsc::Receiver<BridgedRequest>,
Persistent<Function<'static>>,
)>,
>,
}
impl Registry {
pub fn add(&self, info: ScenarioInfo, body: Persistent<Function<'static>>) {
self.scenarios.lock().unwrap().push((info, body));
}
pub fn add_bridged(
&self,
rx: mpsc::Receiver<BridgedRequest>,
body: Persistent<Function<'static>>,
) {
self.bridged.lock().unwrap().push((rx, body));
}
pub fn set_setup(&self, body: Persistent<Function<'static>>) {
*self.setup.lock().unwrap() = Some(body);
}
pub fn set_teardown(&self, body: Persistent<Function<'static>>) {
*self.teardown.lock().unwrap() = Some(body);
}
fn setup_fn(&self) -> Option<Persistent<Function<'static>>> {
self.setup.lock().unwrap().clone()
}
fn teardown_fn(&self) -> Option<Persistent<Function<'static>>> {
self.teardown.lock().unwrap().clone()
}
fn infos(&self) -> Vec<ScenarioInfo> {
self.scenarios
.lock()
.unwrap()
.iter()
.map(|(i, _)| i.clone())
.collect()
}
}
pub struct JsHost {
rt: AsyncRuntime,
context: AsyncContext,
registry: Arc<Registry>,
source: String,
label: String,
entry_name: String,
}
impl JsHost {
pub fn new(
engine: Arc<EngineCtx>,
source: String,
label: String,
env: EnvVars,
overrides: HashMap<String, String>,
base_dir: PathBuf,
) -> anyhow::Result<Self> {
let rt = AsyncRuntime::new()?;
futures_executor::block_on(rt.set_loader(FsResolver, FsLoader));
let context = futures_executor::block_on(AsyncContext::full(&rt))?;
let entry_name = std::fs::canonicalize(&base_dir)
.unwrap_or_else(|_| base_dir.clone())
.join(
Path::new(&label)
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("scenario.js")),
)
.to_string_lossy()
.into_owned();
#[allow(clippy::arc_with_non_send_sync)]
let registry = Arc::new(Registry::default());
futures_executor::block_on(context.with(|ctx| {
super::bindings::install(&ctx, &engine, ®istry, &env, &overrides, &base_dir)
}))?;
Ok(Self {
rt,
context,
registry,
source,
label,
entry_name,
})
}
pub fn check_syntax(&self) -> Result<(), String> {
let name = self.entry_name.clone();
let source = self.source.clone();
let label = self.label.clone();
futures_executor::block_on(
self.context.async_with(async move |ctx| {
match Module::declare(ctx.clone(), name, source) {
Ok(_) => Ok(()),
Err(e) => Err(super::dsl::core::format_exception(&ctx, e, &label)),
}
}),
)
}
}
impl ScriptHost for JsHost {
fn run_top_level(&mut self) -> TopLevel {
let label = self.label.clone();
let name = self.entry_name.clone();
let source = self.source.clone();
let top: Result<(), String> =
futures_executor::block_on(self.context.async_with(async move |ctx| {
match Module::evaluate(ctx.clone(), name, source) {
Ok(promise) => promise
.into_future::<()>()
.await
.map_err(|e| super::dsl::core::format_exception(&ctx, e, &label)),
Err(e) => Err(super::dsl::core::format_exception(&ctx, e, &label)),
}
}));
let scenarios = self.registry.infos();
if scenarios.is_empty() {
TopLevel::Single(top)
} else {
TopLevel::Suite {
scenarios,
top_error: top.err(),
}
}
}
fn run_scenario(&mut self, name: &str) -> ScenarioResult {
let label = self.label.clone();
let registry = self.registry.clone();
futures_executor::block_on(self.context.async_with(async |ctx| {
let entry = registry
.scenarios
.lock()
.unwrap()
.iter()
.find(|(i, _)| i.name == name)
.map(|(_, f)| f.clone());
let Some(persistent) = entry else {
return ScenarioResult::Failed(format!("scenario `{name}` not registered"));
};
let body: Function = match persistent.restore(&ctx) {
Ok(f) => f,
Err(e) => return ScenarioResult::Failed(format!("restore `{name}`: {e}")),
};
let mut sctx = Value::new_undefined(ctx.clone());
if let Some(setup) = registry.setup_fn() {
if let Ok(f) = setup.restore(&ctx) {
match await_value(&f, ()).await {
Ok(v) => sctx = v,
Err(e) => {
return super::dsl::scenario::classify_scenario_error(&ctx, e, &label);
}
}
}
}
let result = match await_body(&body, (sctx.clone(),)).await {
Ok(()) => ScenarioResult::Passed,
Err(e) => super::dsl::scenario::classify_scenario_error(&ctx, e, &label),
};
if let Some(teardown) = registry.teardown_fn() {
if let Ok(f) = teardown.restore(&ctx) {
let _ = await_body(&f, (sctx,)).await;
}
}
result
}))
}
}
async fn await_body<'js, A>(f: &Function<'js>, args: A) -> rquickjs::Result<()>
where
A: rquickjs::function::IntoArgs<'js>,
{
f.call::<_, rquickjs::promise::MaybePromise<'js>>(args)?
.into_future::<()>()
.await
}
async fn await_value<'js, A>(f: &Function<'js>, args: A) -> rquickjs::Result<Value<'js>>
where
A: rquickjs::function::IntoArgs<'js>,
{
f.call::<_, rquickjs::promise::MaybePromise<'js>>(args)?
.into_future::<Value<'js>>()
.await
}
struct FsResolver;
impl Resolver for FsResolver {
fn resolve<'js>(
&mut self,
_ctx: &JsCtx<'js>,
base: &str,
name: &str,
_attrs: Option<ImportAttributes<'js>>,
) -> rquickjs::Result<String> {
let target = if name.starts_with('.') {
Path::new(base)
.parent()
.unwrap_or_else(|| Path::new("."))
.join(name)
} else {
PathBuf::from(name)
};
let target = if target.extension().is_some() {
target
} else {
target.with_extension("js")
};
std::fs::canonicalize(&target)
.map(|p| p.to_string_lossy().into_owned())
.map_err(|_| JsError::new_resolving(base.to_string(), name.to_string()))
}
}
struct FsLoader;
impl Loader for FsLoader {
fn load<'js>(
&mut self,
ctx: &JsCtx<'js>,
name: &str,
_attrs: Option<ImportAttributes<'js>>,
) -> rquickjs::Result<Module<'js, Declared>> {
let source = std::fs::read(name).map_err(|_| JsError::new_loading(name.to_string()))?;
Module::declare(ctx.clone(), name, source)
}
}
impl Drop for JsHost {
fn drop(&mut self) {
self.registry.scenarios.lock().unwrap().clear();
*self.registry.setup.lock().unwrap() = None;
*self.registry.teardown.lock().unwrap() = None;
self.registry.bridged.lock().unwrap().clear();
let _ = &self.rt;
}
}