use std::{fmt::Write, path::Path};
const SHARED_TOML_FILES: &[&str] = &[
"inline_tmpl_tests.toml",
"inline_control_tests.toml",
"tmpl_param_tests.toml",
"feature_e2e_tests.toml",
];
fn main() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let shared_dir = Path::new(&manifest_dir).join("../../tests/shared");
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_path = Path::new(&out_dir).join("shared_compile_time_gen.rs");
let mut output = String::new();
generate_preamble(&mut output);
let mut test_count = 0u32;
let mut skipped_count = 0u32;
for toml_file in SHARED_TOML_FILES {
let toml_path = shared_dir.join(toml_file);
if !toml_path.exists() {
eprintln!(
"cargo::warning=shared test file not found: {}",
toml_path.display()
);
continue;
}
println!("cargo::rerun-if-changed={}", toml_path.display());
let toml_content = std::fs::read_to_string(&toml_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", toml_path.display()));
let root: toml::Table = toml::from_str(&toml_content)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", toml_path.display()));
let Some(tests) = root.get("tests").and_then(|v| v.as_array()) else {
eprintln!("cargo::warning=no tests array in {toml_file}");
continue;
};
for tc_val in tests {
let tc = tc_val.as_table().expect("test case should be a table");
if let Some(generated) = process_test_case(tc, &mut output) {
if generated {
test_count += 1;
} else {
skipped_count += 1;
}
} else {
skipped_count += 1;
}
}
}
std::fs::write(&out_path, &output)
.unwrap_or_else(|e| panic!("failed to write {}: {e}", out_path.display()));
eprintln!(
"cargo::warning=generated {test_count} compile-time template tests ({skipped_count} skipped: expected_error/file-includes/env)"
);
}
fn generate_preamble(output: &mut String) {
writeln!(
output,
r#"
// AUTO-GENERATED by build.rs — do not edit.
// Tests that every shared template compiles via template!() at compile time
// AND renders correctly at runtime.
use std::sync::Arc;
use md_tmpl::{{Context, Value}};
fn toml_to_value(val: &toml::Value) -> Value {{
match val {{
toml::Value::String(s) => {{
if s == "None" {{
Value::None
}} else if let Some(inner) = s.strip_prefix("Some(").and_then(|r| r.strip_suffix(')')) {{
Value::Str(inner.to_string())
}} else {{
Value::Str(s.clone())
}}
}}
toml::Value::Integer(i) => Value::Int(*i),
toml::Value::Float(f) => Value::Float(*f),
toml::Value::Boolean(b) => Value::Bool(*b),
toml::Value::Datetime(dt) => Value::Str(dt.to_string()),
toml::Value::Array(arr) => {{
Value::List(Arc::new(arr.iter().map(toml_to_value).collect()))
}}
toml::Value::Table(tbl) => {{
let mut map = md_tmpl::__private::HashMap::new();
for (k, v) in tbl {{
map.insert(k.clone(), toml_to_value(v));
}}
Value::Struct(Arc::new(map))
}}
}}
}}
fn toml_to_context(params_toml: &str) -> Context {{
let val: toml::Value = toml::from_str(params_toml).expect("parse params toml");
let mut ctx = Context::new();
if let toml::Value::Table(tbl) = val {{
for (k, v) in tbl {{
ctx.set(&k, toml_to_value(&v));
}}
}}
ctx
}}
"#
)
.unwrap();
}
fn process_test_case(tc: &toml::Table, output: &mut String) -> Option<bool> {
let name = tc
.get("name")
.and_then(|v| v.as_str())
.expect("test must have a name");
if tc.contains_key("expected_error") {
return Some(false);
}
if tc.contains_key("parent_template") {
return Some(false);
}
if tc
.get("skip_compile_time")
.and_then(toml::Value::as_bool)
.unwrap_or(false)
{
return Some(false);
}
let template_src = tc.get("template").and_then(|v| v.as_str())?;
if template_src.ends_with(".tmpl.md") {
return Some(false);
}
let expected_output = tc.get("expected_output").and_then(|v| v.as_str())?;
if template_src.contains("\nenv:") || template_src.contains("\nenv :") {
return Some(false);
}
generate_test_module(name, template_src, expected_output, tc, output);
Some(true)
}
fn generate_test_module(
name: &str,
template_src: &str,
expected_output: &str,
tc: &toml::Table,
output: &mut String,
) {
let mod_name = format!("ct_{name}");
let params_toml = match tc.get("params") {
Some(toml::Value::Table(tbl)) => toml::to_string(tbl)
.unwrap_or_else(|e| panic!("failed to serialize params for {name}: {e}")),
_ => String::new(),
};
let (open_delim, close_delim) = raw_string_delimiters(template_src);
let (exp_open, exp_close) = raw_string_delimiters(expected_output);
let (params_open, params_close) = raw_string_delimiters(¶ms_toml);
writeln!(
output,
r#"mod test_{name} {{
use super::*;
md_tmpl::template!(
{open_delim}{template_src}{close_delim} => {mod_name}
);
#[test]
fn compile_time() {{
let expected = {exp_open}{expected_output}{exp_close};
let params_toml = {params_open}{params_toml}{params_close};
let ctx = toml_to_context(params_toml);
let tmpl = {mod_name}::template();
let output = tmpl
.render_ctx(&ctx)
.unwrap_or_else(|e| panic!("[{name}] render failed: {{e}}"));
assert_eq!(output, expected, "[{name}] compile-time template output mismatch");
// Also verify compile-time and runtime templates produce identical output.
let runtime_tmpl = md_tmpl::Template::from_source(
{open_delim}{template_src}{close_delim}
).unwrap_or_else(|e| panic!("[{name}] runtime parse failed: {{e}}"));
let runtime_output = runtime_tmpl
.render_ctx(&ctx)
.unwrap_or_else(|e| panic!("[{name}] runtime render failed: {{e}}"));
assert_eq!(
output, runtime_output,
"[{name}] compile-time vs runtime output divergence"
);
}}
}}"#,
)
.unwrap();
}
fn raw_string_delimiters(content: &str) -> (String, String) {
let mut hashes = 0;
while content.contains(&format!("\"{}", "#".repeat(hashes))) {
hashes += 1;
}
let h = "#".repeat(hashes);
(format!("r{h}\""), format!("\"{h}"))
}