Move the registry and Lua runtime into the library
main.rs re-declared mod assertions/error/utils, so the domain code
compiled twice and only the binary copy ran the registry — which is why
coverage ignored it and 10 mutants survived there. The binary now
depends on the library and keeps only argument parsing and rendering.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/frork-cli/src/lib.rs b/frork-cli/src/lib.rs
index 4f80e5e..da66d2c 100644
--- a/frork-cli/src/lib.rs
+++ b/frork-cli/src/lib.rs
@@ -13,4 +13,6 @@
pub mod assertions;
pub mod error;
+pub mod registry;
+pub mod runtime;
pub mod utils;
diff --git a/frork-cli/src/main.rs b/frork-cli/src/main.rs
index 7dc302f..15727e7 100644
--- a/frork-cli/src/main.rs
+++ b/frork-cli/src/main.rs
@@ -1,47 +1,14 @@
-// Panic discipline applies to code that handles Fennel scripts and shell
-// output; test code asserts on known-good fixtures.
-#![cfg_attr(
- test,
- allow(
- clippy::unwrap_used,
- clippy::expect_used,
- clippy::panic,
- clippy::indexing_slicing,
- clippy::arithmetic_side_effects
- )
-)]
-
-mod assertions;
-mod error;
-mod utils;
-
-use std::cell::RefCell;
-use std::collections::HashMap;
-use std::rc::Rc;
-
-use assertions::AssertionType;
-use assertions::AssertionTypeFactory;
-use assertions::Brew;
-use assertions::BrewBundle;
-use assertions::Debug;
-use assertions::Directory;
-use assertions::Git;
-use assertions::LuaAssertion;
-use assertions::LuaAssertionType;
-use assertions::Status;
-use assertions::Symlink;
-use assertions::TypedFactory;
use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
use clap_complete::Shell;
-use error::FrorkError;
+use frork_cli::assertions::AssertionType;
+use frork_cli::assertions::Status;
+use frork_cli::runtime::run_code;
+use frork_cli::runtime::run_script;
use miette::IntoDiagnostic as _;
use miette::Result;
use miette::miette;
-use mlua::prelude::*;
-use tracing::info;
-use utils::Utils;
/// CalVer, set by build.rs — the crate version is not what ships.
const VERSION: &str = env!("FRORK_VERSION");
@@ -67,6 +34,9 @@ enum Commands {
}
fn main() -> Result<()> {
+ // Assertion types panic through todo!() for cases that are not implemented
+ // yet; this renders those with the same formatting as recoverable errors.
+ miette::set_panic_hook();
tracing_subscriber::fmt::init();
let cli = Cli::parse();
@@ -144,240 +114,3 @@ fn satisfy(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
}
Ok(())
}
-
-fn run_code(
- code: &str,
- handle_status: impl Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-) -> Result<()> {
- let (lua, frork_module, fennel_module) = setup_lua(handle_status)?;
-
- let ok_fn: LuaFunction = frork_module
- .get("ok")
- .map_err(|e| miette!("Failed to get frork.ok: {e}"))?;
- lua.globals()
- .set("ok", ok_fn)
- .map_err(|e| miette!("Failed to set ok global: {e}"))?;
-
- let eval_fn: LuaFunction = fennel_module
- .get("eval")
- .map_err(|e| miette!("Failed to get fennel.eval: {e}"))?;
-
- eval_fn
- .call::<()>(code)
- .map_err(|e| miette!("Failed to execute fennel code: {e}"))?;
-
- Ok(())
-}
-
-fn run_script(
- script: &str,
- handle_status: impl Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-) -> Result<()> {
- let (lua, _frork_module, fennel_module) = setup_lua(handle_status)?;
-
- // Add script directory to Lua and Fennel search paths.
- let script_path = std::path::Path::new(script);
- if let Some(script_dir) = script_path.parent()
- && let Some(script_dir_str) = script_dir.to_str()
- {
- let package_table: LuaTable = lua
- .globals()
- .get("package")
- .map_err(|e| miette!("Failed to get package table: {e}"))?;
-
- // Add to Lua search path.
- let current_path: String = package_table
- .get("path")
- .map_err(|e| miette!("Failed to get current Lua path: {e}"))?;
- let new_path = format!(
- "{};{}/?.lua;{}/?/init.lua",
- current_path, script_dir_str, script_dir_str
- );
- package_table
- .set("path", new_path)
- .map_err(|e| miette!("Failed to set Lua path: {e}"))?;
-
- // Add to Fennel search path.
- let current_fennel_path: String = fennel_module
- .get("path")
- .unwrap_or_else(|_| "./?.fnl;./?/init.fnl".to_string());
- let new_fennel_path = format!(
- "{};{}/?.fnl;{}/?/init.fnl",
- current_fennel_path, script_dir_str, script_dir_str
- );
- fennel_module
- .set("path", new_fennel_path)
- .map_err(|e| miette!("Failed to set Fennel path: {e}"))?;
- }
-
- lua.load(format!(
- r#"require("fennel").install().dofile("{}")"#,
- script
- ))
- .exec()
- .map_err(|e| miette!("Failed to execute script: {e}"))?;
-
- Ok(())
-}
-
-fn setup_lua(
- handle_status: impl Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-) -> Result<(Lua, LuaTable, LuaTable)> {
- let lua = Lua::new();
-
- let fennel_code = include_str!("../fennel-1.6.0.lua");
- let fennel_module: LuaTable = lua
- .load(fennel_code)
- .eval()
- .map_err(|e| miette!("Failed to load Fennel: {e}"))?;
- lua.register_module("fennel", &fennel_module)
- .map_err(|e| miette!("Failed to register fennel module: {e}"))?;
-
- let frork_table = match Frork::new(handle_status, lua.clone())
- .into_lua(&lua)
- .map_err(|e| miette!("Failed to create frork table: {e}"))?
- {
- LuaValue::Table(table) => table,
- _ => unreachable!(),
- };
- lua.register_module("frork", &frork_table)
- .map_err(|e| miette!("Failed to register frork module: {e}"))?;
-
- Ok((lua, frork_table, fennel_module))
-}
-
-struct Frork<F> {
- // RefCell needed for interior mutability — register() adds new assertion
- // types at runtime when called from Lua/Fennel code.
- registry: RefCell<Registry>,
- handle_status: F,
- lua: Lua,
-}
-
-impl<F> Frork<F> {
- fn new(handle_status: F, lua: Lua) -> Self {
- Self {
- registry: RefCell::new(Registry::default()),
- handle_status,
- lua,
- }
- }
-}
-
-impl<F> Frork<F>
-where
- F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-{
- fn register(&self, name: &str, lua_assertion_type: LuaAssertionType) -> LuaResult<()> {
- self.registry
- .borrow_mut()
- .register(name, lua_assertion_type);
- info!("Registered assertion type: {}", name);
- Ok(())
- }
-
- fn ok(&self, args: LuaMultiValue) -> LuaResult<()> {
- if args.is_empty() {
- return Err(LuaError::external(FrorkError::NoOperation));
- }
-
- let mut args_iter = args.into_iter();
- let assertion_type = args_iter
- .next()
- .and_then(|v| v.to_string().ok())
- .ok_or_else(|| LuaError::external(FrorkError::MissingAssertionType))?;
-
- let assertion_args: LuaMultiValue = args_iter.collect();
-
- let factory = self
- .registry
- .borrow()
- .get_factory(&assertion_type)
- .map_err(LuaError::external)?;
- let assertion = factory
- .create(&self.lua, assertion_args)
- .map_err(LuaError::external)?;
- let status = assertion.status().map_err(LuaError::external)?;
-
- (self.handle_status)(&status, assertion.as_ref()).map_err(LuaError::external)?;
- Ok(())
- }
-}
-
-impl<F> IntoLua for Frork<F>
-where
- F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-{
- fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
- let frork_table = lua.create_table()?;
- let frork = Rc::new(self);
-
- let frork_clone = frork.clone();
- let register_fn = lua.create_function(
- move |_lua, (name, lua_assertion_type): (String, LuaAssertionType)| {
- frork_clone.register(&name, lua_assertion_type)
- },
- )?;
-
- frork_table.set("register", register_fn)?;
-
- let frork_clone = frork.clone();
- let ok_fn = lua.create_function(move |_lua, args: LuaMultiValue| frork_clone.ok(args))?;
- frork_table.set("ok", ok_fn)?;
-
- frork_table.set("utils", Utils {})?;
-
- Ok(LuaValue::Table(frork_table))
- }
-}
-
-#[derive(Default)]
-struct Registry {
- lua_assertion_types: HashMap<String, LuaAssertionType>,
-}
-
-impl Registry {
- fn register(&mut self, name: &str, lua_assertion_type: LuaAssertionType) {
- self.lua_assertion_types
- .insert(name.to_string(), lua_assertion_type);
- }
-
- fn get_factory(&self, assertion_type: &str) -> Result<Box<dyn AssertionTypeFactory>> {
- // Check Lua assertions first.
- if let Some(lua_assertion) = self.lua_assertion_types.get(assertion_type) {
- return Ok(Box::new(LuaAssertionFactory {
- assertion_type: assertion_type.to_string(),
- lua_assertion_type: lua_assertion.clone(),
- }));
- }
-
- // Return factory for built-in types.
- match assertion_type {
- "brew" => Ok(Box::new(TypedFactory::<Brew>::new())),
- "brew-bundle" => Ok(Box::new(TypedFactory::<BrewBundle>::new())),
- "debug" => Ok(Box::new(TypedFactory::<Debug>::new())),
- "directory" => Ok(Box::new(TypedFactory::<Directory>::new())),
- "git" => Ok(Box::new(TypedFactory::<Git>::new())),
- "symlink" => Ok(Box::new(TypedFactory::<Symlink>::new())),
- _ => Err(FrorkError::UnknownAssertionType {
- assertion_type: assertion_type.to_string(),
- }
- .into()),
- }
- }
-}
-
-struct LuaAssertionFactory {
- assertion_type: String,
- lua_assertion_type: LuaAssertionType,
-}
-
-impl AssertionTypeFactory for LuaAssertionFactory {
- fn create(&self, _lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>> {
- Ok(Box::new(LuaAssertion::new(
- &self.assertion_type,
- args,
- self.lua_assertion_type.clone(),
- )))
- }
-}
diff --git a/frork-cli/src/registry.rs b/frork-cli/src/registry.rs
new file mode 100644
index 0000000..549a765
--- /dev/null
+++ b/frork-cli/src/registry.rs
@@ -0,0 +1,212 @@
+use std::collections::HashMap;
+
+use miette::Result;
+use mlua::prelude::*;
+
+use crate::assertions::AssertionType;
+use crate::assertions::AssertionTypeFactory;
+use crate::assertions::Brew;
+use crate::assertions::BrewBundle;
+use crate::assertions::Debug;
+use crate::assertions::Directory;
+use crate::assertions::Git;
+use crate::assertions::LuaAssertion;
+use crate::assertions::LuaAssertionType;
+use crate::assertions::Symlink;
+use crate::assertions::TypedFactory;
+use crate::error::FrorkError;
+
+/// Dispatches an assertion type name to the factory that builds it. Types
+/// registered from Fennel shadow the built-ins.
+#[derive(Default)]
+pub struct Registry {
+ lua_assertion_types: HashMap<String, LuaAssertionType>,
+}
+
+impl Registry {
+ pub fn register(&mut self, name: &str, lua_assertion_type: LuaAssertionType) {
+ self.lua_assertion_types
+ .insert(name.to_string(), lua_assertion_type);
+ }
+
+ pub fn get_factory(&self, assertion_type: &str) -> Result<Box<dyn AssertionTypeFactory>> {
+ // Check Lua assertions first.
+ if let Some(lua_assertion) = self.lua_assertion_types.get(assertion_type) {
+ return Ok(Box::new(LuaAssertionFactory {
+ assertion_type: assertion_type.to_string(),
+ lua_assertion_type: lua_assertion.clone(),
+ }));
+ }
+
+ // Return factory for built-in types.
+ match assertion_type {
+ "brew" => Ok(Box::new(TypedFactory::<Brew>::new())),
+ "brew-bundle" => Ok(Box::new(TypedFactory::<BrewBundle>::new())),
+ "debug" => Ok(Box::new(TypedFactory::<Debug>::new())),
+ "directory" => Ok(Box::new(TypedFactory::<Directory>::new())),
+ "git" => Ok(Box::new(TypedFactory::<Git>::new())),
+ "symlink" => Ok(Box::new(TypedFactory::<Symlink>::new())),
+ _ => Err(FrorkError::UnknownAssertionType {
+ assertion_type: assertion_type.to_string(),
+ }
+ .into()),
+ }
+ }
+}
+
+struct LuaAssertionFactory {
+ assertion_type: String,
+ lua_assertion_type: LuaAssertionType,
+}
+
+impl AssertionTypeFactory for LuaAssertionFactory {
+ fn create(&self, _lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>> {
+ Ok(Box::new(LuaAssertion::new(
+ &self.assertion_type,
+ args,
+ self.lua_assertion_type.clone(),
+ )))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::assertions::Status;
+
+ fn lua_assertion_type(lua: &Lua) -> LuaAssertionType {
+ let value = lua
+ .load(r#"return {status = function() return "ok" end, install = function() end}"#)
+ .eval::<LuaValue>()
+ .unwrap();
+ LuaAssertionType::from_lua(value, lua).unwrap()
+ }
+
+ /// Every built-in name resolves to a factory that builds a working
+ /// assertion, checked through the factory rather than by name alone.
+ #[test]
+ fn test_built_in_types_resolve() {
+ let lua = Lua::new();
+ let registry = Registry::default();
+
+ let cases = [
+ ("brew", LuaMultiValue::new(), "brew"),
+ (
+ "brew-bundle",
+ lua.load(r#"return "/tmp/Brewfile""#).eval().unwrap(),
+ "brew-bundle /tmp/Brewfile",
+ ),
+ ("debug", lua.load(r#"return {}"#).eval().unwrap(), "debug"),
+ (
+ "directory",
+ lua.load(r#"return "/tmp""#).eval().unwrap(),
+ "directory /tmp",
+ ),
+ (
+ "git",
+ lua.load(r#"return "/tmp/r", "https://example.com/r.git""#)
+ .eval()
+ .unwrap(),
+ "git /tmp/r https://example.com/r.git",
+ ),
+ (
+ "symlink",
+ lua.load(r#"return "/tmp/link", "/tmp/source""#)
+ .eval()
+ .unwrap(),
+ "symlink /tmp/link /tmp/source",
+ ),
+ ];
+
+ for (name, args, expected) in cases {
+ let factory = registry
+ .get_factory(name)
+ .unwrap_or_else(|e| panic!("{name} did not resolve: {e}"));
+ let assertion = factory
+ .create(&lua, args)
+ .unwrap_or_else(|e| panic!("{name} failed to build: {e}"));
+ assert_eq!(assertion.to_string(), expected, "for type {name}");
+ }
+ }
+
+ #[test]
+ fn test_unknown_type_is_rejected() {
+ let registry = Registry::default();
+
+ let Err(error) = registry.get_factory("nope") else {
+ panic!("expected an unknown assertion type error"); // cov-excl-line
+ };
+ assert!(error.to_string().contains("Unknown assertion type: nope"));
+ }
+
+ #[test]
+ fn test_registered_types_are_dispatched() {
+ let lua = Lua::new();
+ let mut registry = Registry::default();
+ registry.register("custom", lua_assertion_type(&lua));
+
+ let factory = registry.get_factory("custom").unwrap();
+ let assertion = factory
+ .create(&lua, lua.load(r#"return "arg""#).eval().unwrap())
+ .unwrap();
+
+ assert_eq!(assertion.to_string(), "custom arg");
+ }
+
+ #[test]
+ fn test_registered_types_shadow_built_ins() {
+ let lua = Lua::new();
+ let mut registry = Registry::default();
+
+ // Reports missing for a directory that exists, so the built-in (which
+ // would report Ok for /tmp) cannot produce this result.
+ let shadow = lua
+ .load(
+ r#"return {
+ display = function() return "shadowed" end,
+ status = function() return "missing" end,
+ install = function() end,
+ }"#,
+ )
+ .eval::<LuaValue>()
+ .unwrap();
+ registry.register(
+ "directory",
+ LuaAssertionType::from_lua(shadow, &lua).unwrap(),
+ );
+
+ let factory = registry.get_factory("directory").unwrap();
+ let assertion = factory
+ .create(&lua, lua.load(r#"return "/tmp""#).eval().unwrap())
+ .unwrap();
+
+ assert_eq!(assertion.to_string(), "shadowed");
+ assert!(matches!(assertion.status().unwrap(), Status::Missing));
+ }
+
+ #[test]
+ fn test_re_registering_replaces_the_previous_type() {
+ let lua = Lua::new();
+ let mut registry = Registry::default();
+
+ registry.register("custom", lua_assertion_type(&lua));
+ let replacement = lua
+ .load(
+ r#"return {
+ display = function() return "replaced" end,
+ status = function() return "ok" end,
+ install = function() end,
+ }"#,
+ )
+ .eval::<LuaValue>()
+ .unwrap();
+ registry.register(
+ "custom",
+ LuaAssertionType::from_lua(replacement, &lua).unwrap(),
+ );
+
+ let factory = registry.get_factory("custom").unwrap();
+ let assertion = factory.create(&lua, LuaMultiValue::new()).unwrap();
+ assert_eq!(assertion.to_string(), "replaced");
+ }
+}
diff --git a/frork-cli/src/runtime.rs b/frork-cli/src/runtime.rs
new file mode 100644
index 0000000..bc164da
--- /dev/null
+++ b/frork-cli/src/runtime.rs
@@ -0,0 +1,359 @@
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use miette::Result;
+use miette::miette;
+use mlua::prelude::*;
+use tracing::info;
+
+use crate::assertions::AssertionType;
+use crate::assertions::LuaAssertionType;
+use crate::assertions::Status;
+use crate::error::FrorkError;
+use crate::registry::Registry;
+use crate::utils::Utils;
+
+/// Handles each assertion's status once it has been evaluated. The binary
+/// supplies the rendering and any prompting; nothing here writes to stdout.
+pub trait StatusHandler: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static {}
+
+impl<T> StatusHandler for T where T: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static {}
+
+/// Evaluates inline Fennel code.
+pub fn run_code(code: &str, handle_status: impl StatusHandler) -> Result<()> {
+ let (lua, frork_module, fennel_module) = setup_lua(handle_status)?;
+
+ let ok_fn: LuaFunction = frork_module
+ .get("ok")
+ .map_err(|e| miette!("Failed to get frork.ok: {e}"))?;
+ lua.globals()
+ .set("ok", ok_fn)
+ .map_err(|e| miette!("Failed to set ok global: {e}"))?;
+
+ let eval_fn: LuaFunction = fennel_module
+ .get("eval")
+ .map_err(|e| miette!("Failed to get fennel.eval: {e}"))?;
+
+ eval_fn
+ .call::<()>(code)
+ .map_err(|e| miette!("Failed to execute fennel code: {e}"))?;
+
+ Ok(())
+}
+
+/// Evaluates a `.fnl` script file, with its directory on the search path.
+pub fn run_script(script: &str, handle_status: impl StatusHandler) -> Result<()> {
+ let (lua, _frork_module, fennel_module) = setup_lua(handle_status)?;
+
+ // Add script directory to Lua and Fennel search paths.
+ let script_path = std::path::Path::new(script);
+ if let Some(script_dir) = script_path.parent()
+ && let Some(script_dir_str) = script_dir.to_str()
+ {
+ let package_table: LuaTable = lua
+ .globals()
+ .get("package")
+ .map_err(|e| miette!("Failed to get package table: {e}"))?;
+
+ // Add to Lua search path.
+ let current_path: String = package_table
+ .get("path")
+ .map_err(|e| miette!("Failed to get current Lua path: {e}"))?;
+ let new_path = format!(
+ "{};{}/?.lua;{}/?/init.lua",
+ current_path, script_dir_str, script_dir_str
+ );
+ package_table
+ .set("path", new_path)
+ .map_err(|e| miette!("Failed to set Lua path: {e}"))?;
+
+ // Add to Fennel search path.
+ let current_fennel_path: String = fennel_module
+ .get("path")
+ .unwrap_or_else(|_| "./?.fnl;./?/init.fnl".to_string());
+ let new_fennel_path = format!(
+ "{};{}/?.fnl;{}/?/init.fnl",
+ current_fennel_path, script_dir_str, script_dir_str
+ );
+ fennel_module
+ .set("path", new_fennel_path)
+ .map_err(|e| miette!("Failed to set Fennel path: {e}"))?;
+ }
+
+ lua.load(format!(
+ r#"require("fennel").install().dofile("{}")"#,
+ script
+ ))
+ .exec()
+ .map_err(|e| miette!("Failed to execute script: {e}"))?;
+
+ Ok(())
+}
+
+fn setup_lua(handle_status: impl StatusHandler) -> Result<(Lua, LuaTable, LuaTable)> {
+ let lua = Lua::new();
+
+ let fennel_code = include_str!("../fennel-1.6.0.lua");
+ let fennel_module: LuaTable = lua
+ .load(fennel_code)
+ .eval()
+ .map_err(|e| miette!("Failed to load Fennel: {e}"))?;
+ lua.register_module("fennel", &fennel_module)
+ .map_err(|e| miette!("Failed to register fennel module: {e}"))?;
+
+ let frork_value = Frork::new(handle_status, lua.clone())
+ .into_lua(&lua)
+ .map_err(|e| miette!("Failed to create frork table: {e}"))?;
+ let LuaValue::Table(frork_table) = frork_value else {
+ // Frork::into_lua always returns a table.
+ unreachable!();
+ };
+ lua.register_module("frork", &frork_table)
+ .map_err(|e| miette!("Failed to register frork module: {e}"))?;
+
+ Ok((lua, frork_table, fennel_module))
+}
+
+struct Frork<F> {
+ // RefCell needed for interior mutability — register() adds new assertion
+ // types at runtime when called from Lua/Fennel code.
+ registry: RefCell<Registry>,
+ handle_status: F,
+ lua: Lua,
+}
+
+impl<F> Frork<F> {
+ fn new(handle_status: F, lua: Lua) -> Self {
+ Self {
+ registry: RefCell::new(Registry::default()),
+ handle_status,
+ lua,
+ }
+ }
+}
+
+impl<F: StatusHandler> Frork<F> {
+ fn register(&self, name: &str, lua_assertion_type: LuaAssertionType) -> LuaResult<()> {
+ self.registry
+ .borrow_mut()
+ .register(name, lua_assertion_type);
+ info!("Registered assertion type: {}", name);
+ Ok(())
+ }
+
+ fn ok(&self, args: LuaMultiValue) -> LuaResult<()> {
+ if args.is_empty() {
+ return Err(LuaError::external(FrorkError::NoOperation));
+ }
+
+ let mut args_iter = args.into_iter();
+ let assertion_type = args_iter
+ .next()
+ .and_then(|v| v.to_string().ok())
+ .ok_or_else(|| LuaError::external(FrorkError::MissingAssertionType))?;
+
+ let assertion_args: LuaMultiValue = args_iter.collect();
+
+ let factory = self
+ .registry
+ .borrow()
+ .get_factory(&assertion_type)
+ .map_err(LuaError::external)?;
+ let assertion = factory
+ .create(&self.lua, assertion_args)
+ .map_err(LuaError::external)?;
+ let status = assertion.status().map_err(LuaError::external)?;
+
+ (self.handle_status)(&status, assertion.as_ref()).map_err(LuaError::external)?;
+ Ok(())
+ }
+}
+
+impl<F: StatusHandler> IntoLua for Frork<F> {
+ fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
+ let frork_table = lua.create_table()?;
+ let frork = Rc::new(self);
+
+ let frork_clone = frork.clone();
+ // The &Lua annotation is required: without it the closure infers a
+ // single concrete lifetime and no longer satisfies create_function.
+ let register = move |_lua: &Lua, (name, assertion_type): (String, LuaAssertionType)| {
+ frork_clone.register(&name, assertion_type)
+ };
+ let register_fn = lua.create_function(register)?;
+ frork_table.set("register", register_fn)?;
+
+ let frork_clone = frork.clone();
+ let ok_fn = lua.create_function(move |_lua, args: LuaMultiValue| frork_clone.ok(args))?;
+ frork_table.set("ok", ok_fn)?;
+
+ frork_table.set("utils", Utils {})?;
+
+ Ok(LuaValue::Table(frork_table))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use fs_err as fs;
+ use tempfile::TempDir;
+
+ type Recorded = Rc<RefCell<Vec<String>>>;
+
+ /// A status handler that records what it was handed, standing in for the
+ /// binary's printing.
+ fn recorder() -> (Recorded, impl StatusHandler) {
+ let seen: Recorded = Rc::new(RefCell::new(Vec::new()));
+ let sink = seen.clone();
+ let handler = move |status: &Status, assertion: &dyn AssertionType| {
+ sink.borrow_mut().push(format!("{status:?}: {assertion}"));
+ Ok(())
+ };
+ (seen, handler)
+ }
+
+ #[test]
+ fn test_run_code_evaluates_fennel_and_reports_status() {
+ let (seen, handler) = recorder();
+
+ run_code(r#"(ok :directory "/tmp")"#, handler).unwrap();
+
+ assert_eq!(seen.borrow().as_slice(), ["Ok: directory /tmp"]);
+ }
+
+ #[test]
+ fn test_run_code_reports_each_assertion_in_order() {
+ let (seen, handler) = recorder();
+
+ run_code(
+ r#"(ok :directory "/tmp")
+ (ok :directory "/frork-does-not-exist")"#,
+ handler,
+ )
+ .unwrap();
+
+ assert_eq!(
+ seen.borrow().as_slice(),
+ [
+ "Ok: directory /tmp",
+ "Missing: directory /frork-does-not-exist",
+ ]
+ );
+ }
+
+ #[test]
+ fn test_register_adds_a_type_usable_from_fennel() {
+ let (seen, handler) = recorder();
+
+ run_code(
+ r#"(local frork (require :frork))
+ (frork.register :always-missing
+ {:status (fn [] "missing")
+ :install (fn [] nil)
+ :display (fn [a] (.. "custom " a))})
+ (ok :always-missing "thing")"#,
+ handler,
+ )
+ .unwrap();
+
+ assert_eq!(seen.borrow().as_slice(), ["Missing: custom thing"]);
+ }
+
+ #[test]
+ fn test_ok_without_arguments_is_rejected() {
+ let (_seen, handler) = recorder();
+
+ let error = run_code("(ok)", handler).unwrap_err();
+ assert!(error.to_string().contains("Failed to execute fennel code"));
+ }
+
+ #[test]
+ fn test_ok_rejects_unknown_assertion_types() {
+ let (seen, handler) = recorder();
+
+ assert!(run_code(r#"(ok :not-a-real-type)"#, handler).is_err());
+ assert!(seen.borrow().is_empty());
+ }
+
+ #[test]
+ fn test_run_code_propagates_handler_failures() {
+ let handler =
+ |_status: &Status, _assertion: &dyn AssertionType| Err(miette!("handler said no"));
+
+ assert!(run_code(r#"(ok :directory "/tmp")"#, handler).is_err());
+ }
+
+ #[test]
+ fn test_run_code_rejects_invalid_fennel() {
+ let (_seen, handler) = recorder();
+
+ assert!(run_code("(this is not (valid", handler).is_err());
+ }
+
+ #[test]
+ fn test_frork_utils_are_exposed_to_fennel() {
+ let (seen, handler) = recorder();
+
+ run_code(
+ r#"(local frork (require :frork))
+ (ok :directory (frork.utils.dirname "/tmp/b.txt"))"#,
+ handler,
+ )
+ .unwrap();
+
+ assert_eq!(seen.borrow().as_slice(), ["Ok: directory /tmp"]);
+ }
+
+ #[test]
+ fn test_run_script_evaluates_a_file() {
+ let dir = TempDir::new().unwrap();
+ let script = dir.path().join("check.fnl");
+ fs::write(
+ &script,
+ r#"(local frork (require :frork))
+ (frork.ok :directory "/tmp")"#,
+ )
+ .unwrap();
+
+ let (seen, handler) = recorder();
+ run_script(script.to_str().unwrap(), handler).unwrap();
+
+ assert_eq!(seen.borrow().as_slice(), ["Ok: directory /tmp"]);
+ }
+
+ #[test]
+ fn test_run_script_puts_the_script_directory_on_the_search_path() {
+ let dir = TempDir::new().unwrap();
+ fs::write(dir.path().join("helper.fnl"), r#"{:target "/tmp"}"#).unwrap();
+ let script = dir.path().join("main.fnl");
+ fs::write(
+ &script,
+ r#"(local frork (require :frork))
+ (local helper (require :helper))
+ (frork.ok :directory helper.target)"#,
+ )
+ .unwrap();
+
+ let (seen, handler) = recorder();
+ run_script(script.to_str().unwrap(), handler).unwrap();
+
+ assert_eq!(seen.borrow().as_slice(), ["Ok: directory /tmp"]);
+ }
+
+ #[test]
+ fn test_run_script_without_a_parent_directory() {
+ let (_seen, handler) = recorder();
+
+ // An empty path has no parent, so the search-path block is skipped.
+ assert!(run_script("", handler).is_err());
+ }
+
+ #[test]
+ fn test_run_script_reports_a_missing_file() {
+ let (_seen, handler) = recorder();
+
+ let error = run_script("/frork-does-not-exist/nope.fnl", handler).unwrap_err();
+ assert!(error.to_string().contains("Failed to execute script"));
+ }
+}