diff --git a/slhx-derive/tests/compile_fail.rs b/slhx-derive/tests/compile_fail.rs new file mode 100644 index 0000000..41bab05 --- /dev/null +++ b/slhx-derive/tests/compile_fail.rs @@ -0,0 +1,106 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[test] +fn handler_macro_reports_unknown_handle_and_bad_shape() { + // req: derive_handler/001 req: test/003 + let fixture = Fixture::new("slhx-derive-handler-fail"); + fixture.write( + "Cargo.toml", + &format!( + r#"[package] +name = "slhx-derive-handler-fail" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies] +slhx = {{ path = {:?} }} +"#, + repo_path("slhx") + ), + ); + fixture.write( + "build.rs", + r#"fn main() { + let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + std::fs::write( + out.join("slhx.syms"), + "slhx-syms-v1\nhandle\ttemplates/app.heml::known\tknown\t1\n", + ) + .unwrap(); +} +"#, + ); + fixture.write( + "src/lib.rs", + r#"#[slhx::handler] +fn missing() -> impl slhx::IntoEffect { + slhx::EffectBatch::default() +} + +#[slhx::handler] +fn known() {} +"#, + ); + + let output = Command::new("cargo") + .arg("check") + .arg("--quiet") + .arg("--manifest-path") + .arg(fixture.path.join("Cargo.toml")) + .env("CARGO_TARGET_DIR", fixture.path.join("target")) + .output() + .expect("cargo check fixture runs"); + + assert!(!output.status.success(), "fixture unexpectedly compiled"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("unknown slhx handle `missing`; add `data-slhx-handle=\"missing\"`"), + "missing unknown-handle diagnostic in stderr:\n{stderr}" + ); + assert!( + stderr.contains("slhx handler `known` must accept a form/context parameter or return a value implementing IntoEffect"), + "missing handler-shape diagnostic in stderr:\n{stderr}" + ); +} + +fn repo_path(crate_name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crate has workspace parent") + .join(crate_name) +} + +struct Fixture { + path: PathBuf, +} + +impl Fixture { + fn new(name: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "{name}-{}-{}", + std::process::id(), + std::thread::current().name().unwrap_or("test") + )); + let _ = std::fs::remove_dir_all(&path); + std::fs::create_dir_all(path.join("src")).unwrap(); + Self { path } + } + + fn write(&self, relative: &str, contents: &str) { + let path = self.path.join(relative); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).unwrap(); + } + std::fs::write(path, contents).unwrap(); + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.path); + } +}