use crate::engine::{ScriptHost, TopLevel};
use rhai::{AST, Dynamic, Engine, FnPtr, Scope};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock, Weak};
#[derive(Default)]
pub(super) struct Registry {
scenarios: Mutex<Vec<(String, FnPtr)>>,
setup: Mutex<Option<FnPtr>>,
teardown: Mutex<Option<FnPtr>>,
engine: OnceLock<Weak<Engine>>,
ast: OnceLock<Arc<AST>>,
}
impl Registry {
pub(super) fn add_scenario(&self, name: String, body: FnPtr) {
self.scenarios.lock().unwrap().push((name, body));
}
pub(super) fn set_setup(&self, body: FnPtr) {
*self.setup.lock().unwrap() = Some(body);
}
pub(super) fn set_teardown(&self, body: FnPtr) {
*self.teardown.lock().unwrap() = Some(body);
}
fn take_scenarios(&self) -> Vec<(String, FnPtr)> {
std::mem::take(&mut self.scenarios.lock().unwrap())
}
fn setup_fn(&self) -> Option<FnPtr> {
self.setup.lock().unwrap().clone()
}
fn teardown_fn(&self) -> Option<FnPtr> {
self.teardown.lock().unwrap().clone()
}
pub(super) fn set_exec(&self, engine: Weak<Engine>, ast: Arc<AST>) {
let _ = self.engine.set(engine);
let _ = self.ast.set(ast);
}
pub(super) fn exec(&self) -> Option<(Arc<Engine>, Arc<AST>)> {
Some((self.engine.get()?.upgrade()?, self.ast.get()?.clone()))
}
}
pub(super) struct RhaiHost {
engine: Arc<Engine>,
ast: Arc<AST>,
registry: Arc<Registry>,
overrides: HashMap<String, String>,
scenarios: Vec<(String, FnPtr)>,
setup: Option<FnPtr>,
teardown: Option<FnPtr>,
}
impl RhaiHost {
pub(super) fn new(
engine: Arc<Engine>,
ast: Arc<AST>,
registry: Arc<Registry>,
overrides: HashMap<String, String>,
) -> Self {
Self {
engine,
ast,
registry,
overrides,
scenarios: Vec::new(),
setup: None,
teardown: None,
}
}
}
impl ScriptHost for RhaiHost {
fn run_top_level(&mut self) -> TopLevel {
let mut scope = Scope::new();
for (key, value) in self.overrides.drain() {
scope.push_constant(key, value);
}
let top = self.engine.run_ast_with_scope(&mut scope, &self.ast);
self.scenarios = self.registry.take_scenarios();
self.setup = self.registry.setup_fn();
self.teardown = self.registry.teardown_fn();
if self.scenarios.is_empty() {
TopLevel::Single(top.map(|_| ()).map_err(|e| e.to_string()))
} else {
TopLevel::Suite {
names: self.scenarios.iter().map(|(n, _)| n.clone()).collect(),
top_error: top.err().map(|e| e.to_string()),
}
}
}
fn run_scenario(&mut self, name: &str) -> Result<(), String> {
let Some((_, body)) = self.scenarios.iter().find(|(n, _)| n == name) else {
return Err(format!("scenario `{name}` not registered"));
};
let body = body.clone();
run_one(&self.engine, &self.ast, &self.setup, &self.teardown, &body)
}
}
fn run_one(
engine: &Engine,
ast: &AST,
setup: &Option<FnPtr>,
teardown: &Option<FnPtr>,
body: &FnPtr,
) -> Result<(), String> {
let ctx = match setup {
Some(s) => s
.call::<Dynamic>(engine, ast, ())
.map_err(|e| format!("setup: {e}"))?,
None => Dynamic::UNIT,
};
let result = call_with_ctx(engine, ast, body, ctx.clone());
if let Some(t) = teardown {
let _ = call_with_ctx(engine, ast, t, ctx);
}
result
}
fn call_with_ctx(engine: &Engine, ast: &AST, f: &FnPtr, ctx: Dynamic) -> Result<(), String> {
let takes_arg = ast
.iter_functions()
.find(|m| m.name == f.fn_name())
.is_some_and(|m| !m.params.is_empty());
let res = if takes_arg {
f.call::<Dynamic>(engine, ast, (ctx,))
} else {
f.call::<Dynamic>(engine, ast, ())
};
res.map(|_| ()).map_err(|e| e.to_string())
}
pub fn scenario_names(path: &Path) -> Vec<String> {
let Ok(src) = std::fs::read_to_string(path) else {
return Vec::new();
};
let Ok(ast) = Engine::new().compile(&src) else {
return Vec::new();
};
let mut names = Vec::new();
let mut collect = |call: &rhai::FnCallExpr| {
if call.name != "scenario" {
return;
}
if let Some(rhai::Expr::StringConstant(name, _)) = call.args.first() {
names.push(name.to_string());
}
};
ast.walk(&mut |nodes| {
match nodes.last() {
Some(rhai::ASTNode::Stmt(rhai::Stmt::FnCall(call, _))) => collect(call),
Some(rhai::ASTNode::Expr(rhai::Expr::FnCall(call, _))) => collect(call),
_ => {}
}
true });
names
}
pub fn dir_should_run(path: &Path) -> bool {
let Ok(src) = std::fs::read_to_string(path) else {
return true; };
let Ok(ast) = Engine::new().compile(&src) else {
return true; };
let mut found = false;
ast.walk(&mut |nodes| {
let call = match nodes.last() {
Some(rhai::ASTNode::Stmt(rhai::Stmt::FnCall(c, _))) => Some(c),
Some(rhai::ASTNode::Expr(rhai::Expr::FnCall(c, _))) => Some(c),
_ => None,
};
if call.is_some_and(|c| c.name == "scenario") {
found = true;
}
!found });
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scenario_names_from_ast() {
let dir = std::env::temp_dir().join("ringo_flow_sc_test.rhai");
std::fs::write(
&dir,
"setup(|| #{});\nscenario(\"answered call\", |ctx| {});\n// scenario(\"commented out\", ||{});\nscenario(\"rejected call\", || {});\n",
)
.unwrap();
let names = scenario_names(&dir);
assert_eq!(names, vec!["answered call", "rejected call"]);
}
#[test]
fn dir_should_run_skips_helpers_keeps_scenarios_and_broken() {
let write = |name: &str, body: &str| {
let p = std::env::temp_dir().join(name);
std::fs::write(&p, body).unwrap();
p
};
assert!(dir_should_run(&write(
"rf_dsr_scn.rhai",
"scenario(\"x\", || {});"
)));
assert!(!dir_should_run(&write(
"rf_dsr_helper.rhai",
"fn greet(x) { x }\nlet K = 1;"
)));
assert!(dir_should_run(&write(
"rf_dsr_broken.rhai",
"let mut x = 1;"
)));
}
}