Skip to main content

tatara_rust_test/
lib.rs

1//! `tatara-rust-test` — L3 scaffolding.
2//!
3//! Given a typed [`tatara_rust_derive::ProcDeriveSpec`], materialize a
4//! test environment on disk that exercises the generated proc-macro
5//! end-to-end:
6//! - emit the derive crate to a temp dir
7//! - emit a minimal consumer crate that depends on it
8//! - exposes paths for an external driver to run `cargo test`
9//!
10//! Snapshot-friendly: [`as_files`] returns the in-memory file map for
11//! `insta::assert_yaml_snapshot!`.
12
13pub mod example_pack;
14pub use example_pack::{DeriveExamplePackSpec, Example, PackRunReport};
15
16use std::collections::BTreeMap;
17use std::path::{Path, PathBuf};
18use tatara_rust_ast::{AstError, CompileToCrate, CrateScaffold};
19use tatara_rust_derive::ProcDeriveSpec;
20use thiserror::Error;
21
22#[derive(Debug, Error)]
23pub enum TestError {
24    #[error("ast: {0}")]
25    Ast(#[from] AstError),
26    #[error("io: {0}")]
27    Io(#[from] std::io::Error),
28}
29
30/// Lay out a two-crate scaffold (derive + consumer) under `root`.
31pub struct TestLayout {
32    pub root: PathBuf,
33    pub derive_crate_name: String,
34    pub consumer_crate_name: String,
35}
36
37impl TestLayout {
38    pub fn write(
39        spec: &ProcDeriveSpec,
40        derive_crate_name: &str,
41        consumer_crate_name: &str,
42        root: &Path,
43    ) -> Result<Self, TestError> {
44        let derive_root = root.join(derive_crate_name);
45        spec.compile_to_crate(derive_crate_name)?.write_to(&derive_root)?;
46
47        let consumer = consumer_scaffold(spec, derive_crate_name, consumer_crate_name);
48        let consumer_root = root.join(consumer_crate_name);
49        consumer.write_to(&consumer_root)?;
50
51        Ok(Self {
52            root: root.to_path_buf(),
53            derive_crate_name: derive_crate_name.into(),
54            consumer_crate_name: consumer_crate_name.into(),
55        })
56    }
57
58    /// In-memory view of the two scaffolds — useful for snapshot tests
59    /// without touching disk.
60    #[must_use]
61    pub fn as_files(
62        spec: &ProcDeriveSpec,
63        derive_crate_name: &str,
64        consumer_crate_name: &str,
65    ) -> BTreeMap<String, String> {
66        let mut out: BTreeMap<String, String> = BTreeMap::new();
67        if let Ok(d) = spec.compile_to_crate(derive_crate_name) {
68            for f in d.files {
69                out.insert(format!("{derive_crate_name}/{}", f.path), f.contents);
70            }
71        }
72        let c = consumer_scaffold(spec, derive_crate_name, consumer_crate_name);
73        for f in c.files {
74            out.insert(format!("{consumer_crate_name}/{}", f.path), f.contents);
75        }
76        out
77    }
78}
79
80fn consumer_scaffold(
81    spec: &ProcDeriveSpec,
82    derive_crate_name: &str,
83    consumer_crate_name: &str,
84) -> CrateScaffold {
85    let trait_id = &spec.trait_name.0;
86    let derive_under = derive_crate_name.replace('-', "_");
87    let consumer_under = consumer_crate_name.replace('-', "_");
88
89    let mut s = CrateScaffold::new(consumer_crate_name, "0.1.0");
90
91    s.add_file(
92        "Cargo.toml",
93        format!(
94            r#"[package]
95name = "{consumer_crate_name}"
96version = "0.1.0"
97edition = "2024"
98
99[dependencies]
100{derive_crate_name} = {{ path = "../{derive_crate_name}" }}
101
102[lib]
103path = "src/lib.rs"
104"#
105        ),
106    );
107
108    s.add_file(
109        "src/lib.rs",
110        format!(
111            r#"// Consumer crate — exercises the generated derive macro end-to-end.
112use {derive_under}::{trait_id};
113
114#[derive({trait_id})]
115pub struct Thing;
116
117#[cfg(test)]
118mod tests {{
119    use super::*;
120    #[test]
121    fn smoke() {{
122        // Compilation of the derive IS the assertion.
123        let _t = Thing;
124    }}
125}}
126"#
127        ),
128    );
129    let _ = consumer_under; // currently unused; keep for forward compat.
130    s
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use tatara_rust_ast::{
137        Block, Expr, Fn as RsFn, FnSig, Generics, Ident, RefKind, Stmt, TypeRef,
138    };
139
140    fn spec() -> ProcDeriveSpec {
141        ProcDeriveSpec::new(
142            "Probe",
143            vec![RsFn {
144                sig: FnSig {
145                    name: Ident::new("probe"),
146                    generics: Generics::default(),
147                    params: vec![],
148                    return_type: Some(TypeRef {
149                        ident: Ident::new("str"),
150                        generics: vec![],
151                        reference: Some(RefKind::shared_lifetime("static")),
152                    }),
153                },
154                body: Block {
155                    stmts: vec![Stmt::Tail {
156                        expr: Expr::Literal {
157                            value: "\"ok\"".into(),
158                        },
159                    }],
160                },
161            }],
162        )
163    }
164
165    #[test]
166    fn as_files_contains_both_crates() {
167        let files = TestLayout::as_files(&spec(), "probe-derive", "probe-consumer");
168        assert!(files.contains_key("probe-derive/Cargo.toml"));
169        assert!(files.contains_key("probe-derive/src/lib.rs"));
170        assert!(files.contains_key("probe-consumer/Cargo.toml"));
171        assert!(files.contains_key("probe-consumer/src/lib.rs"));
172    }
173
174    #[test]
175    fn consumer_uses_derive_path_dep() {
176        let files = TestLayout::as_files(&spec(), "probe-derive", "probe-consumer");
177        let toml = files.get("probe-consumer/Cargo.toml").unwrap();
178        assert!(toml.contains(r#"probe-derive = { path = "../probe-derive" }"#));
179    }
180}