Test helpers and tests for %def (basic calls, error paths, parameter binding),
%set, and variable substitution.
Test helpers (test_utils.rs)
// <<test utils>>=
// crates/weaveback-macro/src/evaluator/tests/test_utils.rs
use crate::evaluator::{EvalConfig, Evaluator};
use std::path::Path;
/// Create an EvalConfig whose include path lives inside `temp_dir`.
/// Callers must keep the `TempDir` alive for the test duration;
/// on drop, Rust cleans up the directory automatically.
pub fn config_in_temp_dir(temp_dir: &Path) -> EvalConfig {
EvalConfig {
include_paths: vec![temp_dir.to_path_buf()],
..Default::default()
}
}
/// Convenience wrapper: evaluator whose working files stay inside `temp_dir`.
pub fn evaluator_in_temp_dir(temp_dir: &Path) -> Evaluator {
Evaluator::new(config_in_temp_dir(temp_dir))
}
// @
Basic macro definition and call (test_macros.rs)
// <<test macros>>=
// crates/weaveback-macro/src/evaluator/tests/test_macros.rs
use crate::macro_api::process_string_defaults;
#[test]
fn test_simple_macro_definition() {
let source = "%def(test_macro, simple text)\n%test_macro()";
let result = process_string_defaults(source).unwrap();
assert_eq!(std::str::from_utf8(&result).unwrap().trim(), "simple text");
}
#[test]
fn test_macro_with_parameters() {
let source = "%def(greet, name, %{Hello, %(name)!%})\n%greet(World)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"Hello, World!"
);
}
#[test]
fn test_param_substitution() {
let source = "%def(test, param, wrap %(param) here)\n%test(value)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"wrap value here"
);
}
#[test]
fn test_param_paren_precedence() {
let source = "%def(test, p, text with %(p) here)\n%test(hello)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"text with hello here"
);
}
#[test]
fn test_complex_param_substitution() {
let source = "%def(format, text, %{\nbefore %(text),\nmiddle %(text))text,\nafter (%(text))\n%})\n%format(hello)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"before hello,\nmiddle hello)text,\nafter (hello)"
);
}
#[test]
fn test_multiple_params_and_block() {
let source =
"%def(test, a, b, c, %{\n1:%(a)\n2:%(b)\n3:%(c)\n%})\n%test(first,\n second,\n third)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"1:first\n2:second\n3:third"
);
}
#[test]
fn test_mixed_simple_and_block_bodies() {
let source = "%def(simple, just text)\n%def(with_param, p,text with %(p))\n%def(complex, %{\ntext with,\nmultiple) lines\n%})\n%simple()\n%with_param(hello)\n%complex()";
let result = process_string_defaults(source).unwrap();
let result_str = std::str::from_utf8(&result).unwrap();
assert!(result_str.contains("just text"));
assert!(result_str.contains("text with hello"));
assert!(result_str.contains("text with,\nmultiple) lines"));
}
#[test]
fn test_nested_macro_calls() {
let source =
"%def(inner, x, %{inner(%(x))%})\n%def(outer, y, %{outer(%inner(%(y)))%})\n%outer(test)";
let result = process_string_defaults(source).unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap().trim(),
"outer(inner(test))"
);
}
#[test]
fn test_scope_isolation() {
let source = "%def(m1, x, %(x))\n%def(m2, x, %m1(other_%(x)))\n%m2(value)";
let result = process_string_defaults(source).unwrap();
assert_eq!(std::str::from_utf8(&result).unwrap().trim(), "other_value");
}
// @
%def error paths (test_def.rs)
// <<test def>>=
// crates/weaveback-macro/src/evaluator/tests/test_def.rs
use crate::evaluator::{EvalConfig, EvalError, Evaluator};
use crate::macro_api::{process_string, process_string_defaults};
#[test]
fn test_def_macro_other_errors() {
// Test missing arguments
let result = process_string_defaults("%def()");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"Expected InvalidUsage error for empty def"
);
// Test single argument
let result = process_string_defaults("%def(foo)");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"Expected InvalidUsage error for def with only name"
);
// Test numeric name
let result = process_string_defaults("%def(123, body)");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"Expected InvalidUsage error for numeric macro name"
);
// Test numeric parameter
let result = process_string_defaults("%def(foo, 123, body)");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"Expected InvalidUsage error for numeric parameter"
);
// Test parameter with equals
let result = process_string_defaults("%def(foo, param=value, body)");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"Expected InvalidUsage error for parameter with equals"
);
}
#[test]
fn test_def_macro_basic() {
let result = process_string_defaults("%def(foo, bar) [%foo()]").unwrap();
assert_eq!(result, b" [bar]");
}
#[test]
fn test_def_macro_with_params() {
let result = process_string_defaults(
"%def(greet, name, message, Hello, %(name)! %(message))\n%greet(Alice, Have a nice day)",
)
.unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap(),
"\nAlice! Have a nice day"
);
}
#[test]
fn test_def_macro_empty_body() {
let result = process_string_defaults("%def(foo, bar,)\n%foo()").unwrap();
assert_eq!(std::str::from_utf8(&result).unwrap(), "\n");
}
#[test]
fn test_def_macro_comma_errors() {
let result = process_string_defaults("%def(foo bar baz, body)");
assert!(matches!(result, Err(EvalError::InvalidUsage(_))));
let result = process_string_defaults("%def(, foo, bar)");
assert!(matches!(result, Err(EvalError::InvalidUsage(_))));
let result = process_string_defaults("%def(foo,, bar, baz)");
assert!(matches!(result, Err(EvalError::InvalidUsage(_))));
}
#[test]
fn test_def_macro_with_comments() {
let result = process_string_defaults(
"%def(greet, %/* greeting macro %*/\n\
name, %// person to greet\n\
msg, %/* message to show %*/\n\
Hello %(name)! %(msg)\n\
)\n\
%greet(Alice, Good morning)",
)
.unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap(),
"\nHello Alice! Good morning\n"
);
}
#[test]
fn test_def_macro_spaces() {
let result = process_string_defaults("%def( foo, bar, baz, output)\n%foo(bar, baz)").unwrap();
assert_eq!(std::str::from_utf8(&result).unwrap(), "\noutput");
}
#[test]
fn test_def_macro_nested() {
let result = process_string_defaults(
"%def(bold, text, **%(text)**)
%def(greet, name, Hello %bold(dear %(name))!)
%greet(World)",
)
.unwrap();
assert_eq!(
std::str::from_utf8(&result).unwrap(),
"\n \n Hello **dear World**!"
);
}
#[test]
fn test_recursion_depth_limit_returns_error() {
// A directly self-recursive macro must hit MAX_RECURSION_DEPTH and
// return a Runtime error rather than stack-overflowing the process.
let result = process_string_defaults("%def(loop, %loop())\n%loop()");
assert!(
matches!(result, Err(EvalError::Runtime(_))),
"expected Runtime error for infinite recursion, got {:?}", result
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("recursion") || msg.contains("depth"),
"error message should mention recursion depth: {}", msg
);
}
#[test]
fn test_mutual_recursion_depth_limit() {
// Mutually recursive macros: %a calls %b, %b calls %a.
let src = "%def(a, %b())\n%def(b, %a())\n%a()";
let result = process_string_defaults(src);
assert!(
matches!(result, Err(EvalError::Runtime(_))),
"expected Runtime error for mutual recursion, got {:?}", result
);
}
#[test]
fn test_redefine_macro_with_different_script_kind() {
// Redefining a macro with a different ScriptKind (def → rhaidef) should
// silently replace it; the second definition wins.
let src = "%def(compute, x, %(x))\n%rhaidef(compute, x, x + \"!\")\n%compute(hello)";
let result = process_string_defaults(src).unwrap();
let output = String::from_utf8(result).unwrap();
// The rhaidef version appends "!" to its argument.
assert_eq!(output.trim(), "hello!", "rhaidef should shadow earlier %def");
}
#[test]
fn test_param_with_hyphen_is_rejected() {
// Hyphens lex as Special tokens; `single_ident_param` must reject them.
let result = process_string_defaults("%def(foo, my-param, body)");
assert!(
matches!(result, Err(EvalError::InvalidUsage(_))),
"expected InvalidUsage for hyphenated param name, got {:?}", result
);
}
#[test]
fn test_eager_argument_evaluation_order() {
// Arguments are evaluated eagerly (to strings) before the macro body runs,
// but argument evaluation occurs INSIDE the callee's new scope frame.
//
// Consequence: %set inside an argument mutates the callee's scope, not the
// caller's. The caller's scope is unchanged — so `%(counter)` still reads
// the caller's value (0), not the callee-scoped mutation (1).
let src =
"%def(id, x, %(x))\n\
%set(counter, 0)\n\
%id(%set(counter, 1))\n\
%(counter)";
let mut ev = Evaluator::new(EvalConfig::default());
let result = process_string(src, None, &mut ev).unwrap();
let output = String::from_utf8(result).unwrap();
// counter in the caller's (global) scope is unchanged — still 0.
assert!(
output.trim_end().ends_with('0'),
"expected counter=0 (caller scope unchanged), got: {:?}", output
);
}
#[test]
fn test_arguments_evaluated_before_body() {
// Verify strictness: argument expressions are fully expanded to strings
// before the macro body executes.
let src =
"%def(loud, x, %(x)!)\n\
%def(join, a, b, %(a)%(b))\n\
%join(%loud(hi), %loud(there))";
let result = process_string_defaults(src).unwrap();
let output = String::from_utf8(result).unwrap();
assert_eq!(output.trim(), "hi!there!",
"expected eager evaluation: hi! and there! expanded before join body runs");
}
// @
Variable substitution (test_var.rs)
// <<test var>>=
use crate::macro_api::process_string_defaults;
#[test]
fn test_simple_variable_substitution() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%greet(World)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_nested_variable_substitution() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%def(greet_twice, name, %{
%greet(%(name))
%greet(%(name))
%})
%greet_twice(World)
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"Hello, World!\n \n \n Hello, World!"
);
}
#[test]
fn test_variable_substitution_with_whitespace() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%greet( World )
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World !");
}
#[test]
fn test_variable_substitution_with_empty_string() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%greet()
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, !");
}
#[test]
fn test_variable_substitution_with_multiple_arguments() {
let result = process_string_defaults(
r#"
%def(greet, name, greeting, %{
%(greeting), %(name)!
%})
%greet(World, Hello)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_variable_substitution_with_macro_in_argument() {
let result = process_string_defaults(
r#"
%def(get_name, %{
World
%})
%def(greet, name, %{
Hello, %(name)!
%})
%greet(%get_name())
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"Hello, \n World\n !"
);
}
#[test]
fn test_variable_substitution_with_conditional_logic() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
%if(%(name), %{
Hello, %(name)!
%}, %{
Hello, stranger!
%})
%})
%greet(World)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_variable_substitution_with_conditional_logic_empty() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
%if(%(name), %{
Hello, %(name)!
%}, %{
Hello, stranger!
%})
%})
%greet()
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"Hello, stranger!"
);
}
// @
%set (test_set.rs)
// <<test set>>=
#[cfg(test)]
mod tests {
use crate::macro_api::process_string_defaults;
#[test]
fn test_builtin_set() {
// The %set builtin should set variable "foo" to "bar".
// Then, the expression "%(foo)" should expand to "bar".
let source = "%set(foo, bar)%(foo)";
let result =
process_string_defaults(source).expect("Failed to process string with %set builtin");
let output = String::from_utf8(result).expect("Output was not valid UTF-8");
assert_eq!(output.trim(), "bar");
}
}
// @