use std::path::Path;
use std::process::Command;
use macroforge_ts::host::MacroExpander;
use macroforge_ts::host::declarative::BuildMode;
fn deno_available() -> bool {
Command::new("deno")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn run_in_deno(source: &str) -> Result<String, String> {
let tmp_file = tempfile::Builder::new()
.prefix("macroforge-differential-")
.suffix(".ts")
.tempfile()
.map_err(|e| format!("tempfile: {}", e))?;
std::fs::write(tmp_file.path(), source.as_bytes())
.map_err(|e| format!("write tempfile: {}", e))?;
let output = Command::new("deno")
.arg("run")
.arg("--no-check")
.arg("--allow-all")
.arg(tmp_file.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| format!("failed to spawn deno: {}", e))?;
if !output.status.success() {
return Err(format!(
"deno run exited with status {}: stderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn expand_with_mode(source: &str, file_name: &str, mode: BuildMode) -> Result<String, String> {
let mut expander = MacroExpander::new().map_err(|e| format!("expander: {}", e))?;
expander.set_build_mode(mode);
let result = expander
.expand_source(source, file_name)
.map_err(|e| format!("expand ({:?}): {}", mode, e))?;
let errors: Vec<String> = result
.diagnostics
.iter()
.filter(|d| matches!(d.level, macroforge_ts::ts_syn::abi::DiagnosticLevel::Error))
.map(|d| d.message.clone())
.collect();
if !errors.is_empty() {
return Err(format!("{:?} expansion errors: {:?}", mode, errors));
}
Ok(result.code)
}
fn run_differential(path: &Path) {
let input = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("failed to read fixture {:?}: {}", path, e));
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.expect("fixture path")
.to_string();
let dev_code = expand_with_mode(&input, &file_name, BuildMode::dev())
.unwrap_or_else(|e| panic!("{:?}: {}", path, e));
let prod_code = expand_with_mode(&input, &file_name, BuildMode::Prod)
.unwrap_or_else(|e| panic!("{:?}: {}", path, e));
let dev_out = run_in_deno(&dev_code)
.unwrap_or_else(|e| panic!("{:?} dev run failed: {}\ncode:\n{}", path, e, dev_code));
let prod_out = run_in_deno(&prod_code)
.unwrap_or_else(|e| panic!("{:?} prod run failed: {}\ncode:\n{}", path, e, prod_code));
assert_eq!(
dev_out, prod_out,
"differential output mismatch for {:?}\n--- dev stdout ---\n{}\n--- prod stdout ---\n{}\n--- dev code ---\n{}\n--- prod code ---\n{}",
path, dev_out, prod_out, dev_code, prod_code
);
}
#[test]
fn differential_declarative_macros() {
if !deno_available() {
eprintln!(
"[differential] deno not found on PATH — skipping. Install Deno to run this test."
);
return;
}
let fixtures_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("differential");
assert!(
fixtures_dir.exists(),
"fixtures dir not found: {:?}",
fixtures_dir
);
let entries: Vec<_> = std::fs::read_dir(&fixtures_dir)
.expect("read_dir")
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|x| x.to_str())
.is_some_and(|x| x == "ts")
})
.collect();
assert!(
!entries.is_empty(),
"no differential fixtures found in {:?}",
fixtures_dir
);
for entry in entries {
let path = entry.path();
eprintln!("[differential] {:?}", path.file_name().unwrap());
run_differential(&path);
}
}