use std::path::{Path, PathBuf};
use md_tmpl_core::{CompileOptions, Template, Value};
use serde::Deserialize;
use serde_yaml::Value as Yaml;
const CORPUS_FILES: &[&str] = &[
"render.toml",
"interpolation.toml",
"frontmatter.toml",
"errors.toml",
"escapes.toml",
"comments.toml",
"literals.toml",
];
#[derive(Deserialize)]
struct Case {
name: String,
source: String,
#[serde(default)]
params: Yaml,
#[serde(default)]
env: Option<Yaml>,
expect: Expect,
}
#[derive(Deserialize)]
struct Expect {
kind: String,
#[serde(default)]
output: Option<String>,
#[serde(default)]
defaults: Option<Yaml>,
#[serde(default)]
phase: Option<String>,
#[serde(default)]
error_contains: Option<String>,
}
fn corpus_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/conformance")
}
fn load(file: &str) -> Vec<Case> {
#[derive(Deserialize)]
struct Corpus {
cases: Vec<Case>,
}
let path = corpus_dir().join(file);
let txt = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read corpus file {}: {e}", path.display()));
let root: toml::Value = toml::from_str(&txt)
.unwrap_or_else(|e| panic!("parse corpus file {}: {e}", path.display()));
let yaml = toml_to_yaml(&root);
let corpus: Corpus = serde_yaml::from_value(yaml)
.unwrap_or_else(|e| panic!("decode corpus file {}: {e}", path.display()));
corpus.cases
}
fn is_none_sentinel(tbl: &toml::Table) -> bool {
tbl.len() == 1 && matches!(tbl.get("__none__"), Some(toml::Value::Boolean(true)))
}
fn toml_to_yaml(v: &toml::Value) -> Yaml {
match v {
toml::Value::String(s) => Yaml::String(s.clone()),
toml::Value::Integer(i) => Yaml::Number((*i).into()),
toml::Value::Float(f) => Yaml::Number(serde_yaml::Number::from(*f)),
toml::Value::Boolean(b) => Yaml::Bool(*b),
toml::Value::Datetime(dt) => Yaml::String(dt.to_string()),
toml::Value::Array(arr) => Yaml::Sequence(arr.iter().map(toml_to_yaml).collect()),
toml::Value::Table(tbl) => {
if is_none_sentinel(tbl) {
return Yaml::Null;
}
let mut m = serde_yaml::Mapping::new();
for (k, val) in tbl {
m.insert(Yaml::String(k.clone()), toml_to_yaml(val));
}
Yaml::Mapping(m)
}
}
}
fn compile(source: &str, env: Option<&Yaml>) -> Result<Template, String> {
match env {
None => Template::from_source(source).map_err(|e| e.to_string()),
Some(env_yaml) => {
let mapping = env_yaml.as_mapping().expect("env must be a mapping");
let owned: Vec<(String, Value)> = mapping
.iter()
.map(|(k, v)| {
let key = k.as_str().expect("env key must be a string").to_owned();
let val = v.as_str().expect("env value must be a string").to_owned();
(key, Value::Str(val))
})
.collect();
let refs: Vec<(&str, Value)> =
owned.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
Template::compile(source, CompileOptions::default().env(&refs))
.map(|(t, _fm)| t)
.map_err(|e| e.to_string())
}
}
}
fn value_to_yaml(v: &Value) -> Yaml {
match v {
Value::Str(s) => Yaml::String(s.clone()),
Value::Bool(b) => Yaml::Bool(*b),
Value::Int(i) => Yaml::Number(serde_yaml::Number::from(*i)),
Value::Float(f) => Yaml::Number(serde_yaml::Number::from(*f)),
Value::List(items) => Yaml::Sequence(items.iter().map(value_to_yaml).collect()),
Value::Struct(map) => {
let mut m = serde_yaml::Mapping::new();
for (k, val) in map.iter() {
m.insert(Yaml::String(k.clone()), value_to_yaml(val));
}
Yaml::Mapping(m)
}
Value::Tmpl(_) => Yaml::String("<tmpl>".to_owned()),
Value::None => Yaml::Null,
}
}
fn check_render(file: &str, case: &Case, fails: &mut Vec<String>) {
let name = &case.name;
let want = case
.expect
.output
.as_deref()
.expect("render case needs expect.output");
let tmpl = match compile(&case.source, case.env.as_ref()) {
Ok(t) => t,
Err(e) => {
fails.push(format!("[{file}] {name}: Rust COMPILE error: {e}"));
return;
}
};
match tmpl.render(&case.params) {
Ok(got) => {
if got != want {
fails.push(format!(
"[{file}] {name}: render mismatch\n want: {want:?}\n got : {got:?}"
));
}
}
Err(e) => fails.push(format!("[{file}] {name}: Rust RENDER error: {e}")),
}
}
fn check_default(file: &str, case: &Case, fails: &mut Vec<String>) {
let name = &case.name;
let want = case
.expect
.defaults
.as_ref()
.expect("default case needs expect.defaults");
let tmpl = match compile(&case.source, None) {
Ok(t) => t,
Err(e) => {
fails.push(format!("[{file}] {name}: Rust COMPILE error: {e}"));
return;
}
};
let mut m = serde_yaml::Mapping::new();
for (k, v) in &tmpl.defaults() {
m.insert(Yaml::String(k.clone()), value_to_yaml(v));
}
let got = Yaml::Mapping(m);
if &got != want {
fails.push(format!(
"[{file}] {name}: defaults mismatch\n want: {want:?}\n got : {got:?}"
));
}
}
fn check_needle(
file: &str,
name: &str,
kind: &str,
needle: Option<&str>,
haystack: &str,
fails: &mut Vec<String>,
) {
if let Some(n) = needle {
if !haystack.contains(n) {
fails.push(format!(
"[{file}] {name}: Rust {kind} error {haystack:?} lacks substring {n:?}"
));
}
}
}
fn check_error(file: &str, case: &Case, fails: &mut Vec<String>) {
let name = &case.name;
let phase = case
.expect
.phase
.as_deref()
.expect("error case needs phase");
let needle = case.expect.error_contains.as_deref();
let compiled = compile(&case.source, None);
match phase {
"compile" => match compiled {
Ok(_) => fails.push(format!(
"[{file}] {name}: expected COMPILE error but Rust compiled OK"
)),
Err(e) => check_needle(file, name, "compile", needle, &e, fails),
},
"render" => match compiled {
Err(e) => fails.push(format!(
"[{file}] {name}: expected RENDER error but Rust failed at COMPILE: {e}"
)),
Ok(tmpl) => match tmpl.render(&case.params) {
Ok(_) => fails.push(format!(
"[{file}] {name}: expected RENDER error but Rust rendered OK"
)),
Err(e) => check_needle(file, name, "render", needle, &e.to_string(), fails),
},
},
"any" => match compiled {
Err(e) => check_needle(file, name, "compile", needle, &e, fails),
Ok(tmpl) => match tmpl.render(&case.params) {
Ok(_) => fails.push(format!(
"[{file}] {name}: expected an error in either phase but Rust succeeded in both"
)),
Err(e) => check_needle(file, name, "render", needle, &e.to_string(), fails),
},
},
other => fails.push(format!("[{file}] {name}: bad phase {other}")),
}
}
#[test]
fn conformance_corpus_matches_rust_backend() {
let mut fails: Vec<String> = Vec::new();
let mut checked = 0usize;
for file in CORPUS_FILES {
for case in load(file) {
checked += 1;
match case.expect.kind.as_str() {
"render" => check_render(file, &case, &mut fails),
"default" => check_default(file, &case, &mut fails),
"error" => check_error(file, &case, &mut fails),
other => fails.push(format!(
"[{file}] {}: unknown expect.kind {other}",
case.name
)),
}
}
}
assert!(
checked > 0,
"conformance corpus was empty — is {:?} populated?",
corpus_dir()
);
assert!(
fails.is_empty(),
"Rust backend diverges from the shared conformance corpus on {} case(s):\n{}",
fails.len(),
fails.join("\n")
);
}