Test coverage for %if, %equal, %include, %import, %export, %eval,
and %here.
%if conditionals (test_if.rs)
// <<test if>>=
use crate::macro_api::process_string_defaults;
#[test]
fn test_if_condition_true() {
let result = process_string_defaults(
r#"
%if(true, %{
This should be printed.
%})
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"This should be printed."
);
}
#[test]
fn test_if_condition_false() {
let result = process_string_defaults(
r#"
%if( , %{
This should NOT be printed.
%})
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "");
}
#[test]
fn test_if_else_condition_true() {
let result = process_string_defaults(
r#"
%if(true, %{
This should be printed.
%}, %{
This should NOT be printed.
%})
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"This should be printed."
);
}
#[test]
fn test_if_else_condition_false() {
let result = process_string_defaults(
r#"
%if(, %{
This should NOT be printed.
%}, %{
This should be printed.
%})
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"This should be printed."
);
}
#[test]
fn test_nested_if_conditions() {
let result = process_string_defaults(
r#"
%if(true, %{
%if(true, %{
Nested condition is true.
%})
%})
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"Nested condition is true."
);
}
#[test]
fn test_if_with_macro_condition() {
let result = process_string_defaults(
r#"
%def(empty,)
%if(%empty(), , %{condition is false.%})
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"condition is false."
);
}
// @
%equal (test_equal.rs)
// <<test equal>>=
use crate::macro_api::process_string_defaults;
use std::str;
#[test]
fn test_equal_basic() {
let result = process_string_defaults("%equal(abc, abc)").unwrap();
assert_eq!(result, b"abc");
let result = process_string_defaults("%equal(abc, def)").unwrap();
assert_eq!(result, b"");
}
#[test]
fn test_equal_whitespace() {
let result = process_string_defaults("%equal( abc , abc)").unwrap();
assert_eq!(result, b"");
let result = process_string_defaults("%equal( abc , abc )").unwrap();
assert_eq!(str::from_utf8(&result).unwrap().trim(), "abc");
let result = process_string_defaults("%equal(abc , abc )").unwrap();
assert_eq!(str::from_utf8(&result).unwrap().trim(), "abc");
}
#[test]
fn test_equal_with_vars() {
let result = process_string_defaults(
r#"
%def(set_x, val, %(val))
%equal(%set_x(value), value)
"#,
)
.unwrap();
assert_eq!(str::from_utf8(&result).unwrap().trim(), "value");
}
#[test]
fn test_equal_errors() {
assert!(process_string_defaults("%equal()").is_err());
assert!(process_string_defaults("%equal(single)").is_err());
assert!(process_string_defaults("%equal(a,b,c)").is_err());
}
// @
%include (test_include.rs)
// <<test include>>=
// crates/weaveback-macro/src/evaluator/tests/test_include.rs
use crate::evaluator::EvalError;
use crate::evaluator::Evaluator;
use crate::evaluator::tests::test_utils::evaluator_in_temp_dir;
use crate::macro_api::process_string;
use crate::macro_api::process_string_defaults;
use std::fs;
use std::io::Write;
use std::os::unix::fs::symlink;
use std::path::Path;
use tempfile::NamedTempFile;
use tempfile::TempDir;
fn create_evaluator_with_temp_dir(temp_dir: &Path) -> Evaluator {
evaluator_in_temp_dir(temp_dir)
}
#[test]
fn test_include_basic() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let header_path = temp_dir_path.join("header.txt");
fs::write(&header_path, "Hello from header.txt").expect("Failed to write header.txt");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string("%include(header.txt)", None, &mut evaluator).unwrap();
assert_eq!(String::from_utf8(result).unwrap(), "Hello from header.txt");
}
#[test]
fn test_include_with_macros() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let macros_path = temp_dir_path.join("macros.txt");
fs::write(
¯os_path,
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%def(farewell, name, %{
Goodbye, %(name)!
%})
"#,
)
.expect("Failed to write macros.txt");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string(
r#"
%include(macros.txt)
%greet(World)
%farewell(Friend)
"#,
None,
&mut evaluator,
)
.unwrap();
let trimmed_result = String::from_utf8(result).unwrap().trim().to_string();
assert_eq!(
trimmed_result,
"Hello, World!\n \n \n Goodbye, Friend!"
);
}
#[test]
fn test_include_missing_file() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string("%include(missing.txt)", None, &mut evaluator);
assert!(matches!(result, Err(EvalError::IncludeNotFound(_))));
}
#[test]
fn test_include_self_inclusion() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let self_include_path = temp_dir_path.join("self_include.txt");
fs::write(&self_include_path, "%include(self_include.txt)")
.expect("Failed to write self_include.txt");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string("%include(self_include.txt)", None, &mut evaluator);
assert!(matches!(result, Err(EvalError::CircularInclude(_))));
}
#[test]
fn test_include_mutual_inclusion() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let file_a_path = temp_dir_path.join("file_a.txt");
fs::write(&file_a_path, "%include(file_b.txt)").expect("Failed to write file_a.txt");
let file_b_path = temp_dir_path.join("file_b.txt");
fs::write(&file_b_path, "%include(file_a.txt)").expect("Failed to write file_b.txt");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string("%include(file_a.txt)", None, &mut evaluator);
assert!(matches!(result, Err(EvalError::CircularInclude(_))));
}
#[test]
fn test_include_with_symlink() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let target_path = temp_dir_path.join("target.txt");
fs::write(&target_path, "Hello from target.txt").expect("Failed to write target.txt");
let symlink_path = temp_dir_path.join("symlink.txt");
symlink(&target_path, &symlink_path).expect("Failed to create symlink");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string("%include(symlink.txt)", None, &mut evaluator).unwrap();
assert_eq!(String::from_utf8(result).unwrap(), "Hello from target.txt");
}
/// Regression test for Bug 6: open_includes was not cleaned up when an include failed,
/// causing a spurious CircularInclude error on any subsequent include of the same file.
#[test]
fn test_include_path_cleaned_up_on_error() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let bad_file_path = temp_dir_path.join("bad.txt");
fs::write(&bad_file_path, "%undefined_macro()").expect("Failed to write bad.txt");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result1 = process_string("%include(bad.txt)", None, &mut evaluator);
assert!(
matches!(result1, Err(EvalError::UndefinedMacro(_))),
"Expected UndefinedMacro on first include, got: {:?}",
result1
);
let result2 = process_string("%include(bad.txt)", None, &mut evaluator);
assert!(
matches!(result2, Err(EvalError::UndefinedMacro(_))),
"Expected UndefinedMacro on second include (not CircularInclude), got: {:?}",
result2
);
}
#[test]
fn test_include_scope() {
let mut tmp = NamedTempFile::new().expect("Failed to create temp file");
writeln!(tmp, "%def(included_macro, x, Included says: %(x)!)")
.expect("Failed to write to temp file");
let tmp_path = tmp.path().to_str().expect("Invalid temp file path");
let source = format!(
r#"
%def(macro_with_include, param, %{{
%include({tmp_path})
%included_macro(%(param))
%}})
%macro_with_include(test)
%included_macro(outside)
"#
);
let result = process_string_defaults(&source);
match result {
Err(EvalError::UndefinedMacro(m)) => {
assert_eq!(
m, "included_macro",
"Expected UndefinedMacro error for 'included_macro'"
);
}
Err(e) => {
panic!(
"Expected UndefinedMacro error, but got a different error: {:?}",
e
);
}
Ok(output) => {
panic!(
"Expected an error due to undefined macro, but processing succeeded with output: {:?}",
String::from_utf8_lossy(&output)
);
}
}
}
// @
%import (test_import.rs)
// <<test import>>=
#[cfg(test)]
mod tests {
use crate::macro_api::process_string_defaults;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_import_includes_definitions_only() {
let mut tmp = NamedTempFile::new().expect("Failed to create temporary file");
writeln!(tmp, "This text should be discarded.").expect("Failed to write to temp file");
writeln!(tmp, "%def(included_macro, x, included: %(x)!)")
.expect("Failed to write macro definition to temp file");
writeln!(tmp, "More text that should be discarded.").expect("Failed to write to temp file");
let tmp_path = tmp
.path()
.to_str()
.expect("Temporary file path is not valid UTF-8");
let source = format!(
r#"
%def(macro_using_includes, param, %{{
%import({tmp_path})
%included_macro(%(param))
%}})
%macro_using_includes(test)
"#
);
let result = process_string_defaults(&source);
match result {
Ok(output) => {
let output_str = String::from_utf8(output).expect("Output was not valid UTF-8");
let expected = "included: test!";
assert_eq!(
output_str.trim(),
expected,
"Output did not match expected result. Got: {:?}",
output_str
);
}
Err(e) => {
panic!("Processing failed with error: {:?}", e);
}
}
}
}
// @
%export (test_export.rs)
// <<test export>>=
// crates/weaveback-macro/src/evaluator/tests/test_export.rs
#[cfg(test)]
mod tests {
use crate::evaluator::EvalError;
use crate::macro_api::process_string_defaults;
#[test]
fn test_export_macro_with_frozen_args() {
let source = r#"
%def(macro_exporting_stuff, base, name, %{
%def(AFILE, param, %(base)/%(name)%(param).txt)
%export(AFILE)
%set(my_var, %{from %(base) import %(name)%})
%export(my_var)
%})
%macro_exporting_stuff(one, two)
%AFILE(three)
%(my_var)
"#;
let result = process_string_defaults(source)
.expect("Processing failed for export with macro parameters");
let output = String::from_utf8(result).expect("Output was not valid UTF-8");
let expected = "one/twothree.txt\nfrom one import two";
assert_eq!(
output.trim(),
expected,
"Exported macro did not freeze outer variables as expected"
);
}
#[test]
fn test_export_wrong_number_of_args() {
let source = "%export(foo, bar)";
let result = process_string_defaults(source);
match result {
Err(EvalError::InvalidUsage(msg)) => {
assert!(
msg.contains("export: exactly 1 arg"),
"Unexpected error message: {}",
msg
);
}
_ => panic!("Expected an InvalidUsage error when %export is called with two arguments"),
}
}
}
// @
%eval (test_eval.rs)
// <<test eval>>=
use crate::macro_api::process_string_defaults;
#[test]
fn test_eval_simple_macro_call() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%eval(greet, World)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_eval_macro_call_with_multiple_arguments() {
let result = process_string_defaults(
r#"
%def(greet, name, greeting, %{
%(greeting), %(name)!
%})
%eval(greet, World, Hello)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_eval_macro_call_with_nested_macros() {
let result = process_string_defaults(
r#"
%def(get_name, %{
World
%})
%def(greet, name, %{
Hello, %(name)!
%})
%eval(greet, %get_name())
"#,
)
.unwrap();
assert_eq!(
String::from_utf8(result).unwrap().trim(),
"Hello, \n World\n !"
);
}
#[test]
fn test_eval_macro_call_with_conditional_logic() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
%if(%(name), %{
Hello, %(name)!
%}, %{
Hello, stranger!
%})
%})
%eval(greet, World)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
#[test]
fn test_eval_macro_call_with_empty_arguments() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%eval(greet, )
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, !");
}
#[test]
fn test_eval_macro_call_with_whitespace_in_arguments() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%eval(greet, World )
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World !");
}
#[test]
fn test_eval_macro_call_with_dynamic_macro_name() {
let result = process_string_defaults(
r#"
%def(greet, name, %{
Hello, %(name)!
%})
%def(get_macro_name, %{
greet
%})
%eval(%get_macro_name(), World)
"#,
)
.unwrap();
assert_eq!(String::from_utf8(result).unwrap().trim(), "Hello, World!");
}
// @
%here (test_here.rs)
// <<test here>>=
// crates/weaveback-macro/src/evaluator/tests/test_here.rs
use crate::evaluator::Evaluator;
use crate::evaluator::tests::test_utils::evaluator_in_temp_dir;
use crate::macro_api::process_string;
use std::fs;
use tempfile::TempDir;
fn create_evaluator_with_temp_dir(temp_dir: &std::path::Path) -> Evaluator {
evaluator_in_temp_dir(temp_dir)
}
#[test]
fn test_here_with_macros() {
let temp_dir = TempDir::new().expect("Failed to create temporary directory");
let temp_dir_path = temp_dir.path();
let test_file_path = temp_dir_path.join("test.txt");
fs::write(
&test_file_path,
r#"
%def(insert_content, greeting, %{
Inserted content, %(greeting)!
%})
Before %here(insert_content, Hello)
After
"#,
)
.expect("Failed to write test file");
let mut evaluator = create_evaluator_with_temp_dir(temp_dir_path);
let result = process_string(
&fs::read_to_string(&test_file_path).unwrap(),
Some(&test_file_path),
&mut evaluator,
);
let modified_content = fs::read_to_string(&test_file_path).unwrap();
assert_eq!(
modified_content.trim(),
"%def(insert_content, greeting, %{\n Inserted content, %(greeting)!\n %})\n Before %here(insert_content, Hello)\n Inserted content, Hello!\n After"
);
assert!(result.is_ok());
}
#[test]
fn test_here_idempotency_already_patched_file() {
// Running on a file that already has %here (the neutralised form) must
// be a no-op: the file must not be modified a second time.
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("patched.txt");
// Write a file that already has %here (neutralised) — simulating the state
// after a previous successful run.
let already_patched =
"%def(msg, hello world)\n%here(msg)\nhello world\nrest of file";
fs::write(&test_file, already_patched).unwrap();
let content = fs::read_to_string(&test_file).unwrap();
let mut ev = create_evaluator_with_temp_dir(temp_dir.path());
let result = process_string(&content, Some(&test_file), &mut ev);
// Must succeed (%here is literal text, not a macro call).
assert!(result.is_ok(), "already-patched file should expand without error: {:?}", result);
// File must be unchanged — no second patch.
let after = fs::read_to_string(&test_file).unwrap();
assert_eq!(after, already_patched, "idempotency violated: file was modified again");
}
#[test]
fn test_here_only_first_fires_in_file() {
// A file with two %here calls: only the first should fire.
// After the first %here sets early_exit, the second is never reached.
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("two_here.txt");
let content =
"%def(a, first)\n%def(b, second)\n%here(a)\n%here(b)";
fs::write(&test_file, content).unwrap();
let src = fs::read_to_string(&test_file).unwrap();
let mut ev = create_evaluator_with_temp_dir(temp_dir.path());
process_string(&src, Some(&test_file), &mut ev).unwrap();
let modified = fs::read_to_string(&test_file).unwrap();
// Only the first %here should have been neutralised (%here) and expanded.
assert!(modified.contains("%here(a)"), "first %here should be neutralised");
// Second %here must still be a live call (not neutralised) because early_exit
// stopped evaluation before reaching it.
assert!(
modified.contains("%here(b)") && !modified.contains("%here(b)"),
"second %here should be untouched (early_exit prevented it from firing)"
);
}
// @