Skip to main content

tatara_rust_test/
example_pack.rs

1//! `DeriveExamplePackSpec` — typed test-by-construction primitive.
2//!
3//! Pairs a derive Spec with N `(consumer_source, assertion_block)`
4//! pairs. Each pack materializes a temp workspace + consumer crate
5//! that imports the derive, applies it to the consumer source, and
6//! runs the assertion block via `cargo test`. The Spec ships with its
7//! own proof of correctness.
8//!
9//! Same shape works for any `CompileToCrate` Spec (Derive, PerField,
10//! PerVariant, Composite). The pack is generic over the kind via a
11//! boxed `&dyn CompileToCrate` reference.
12//!
13//! Real usage in `crates/tatara-rust-test/tests/example_pack_e2e.rs`.
14
15use std::path::Path;
16use std::process::Command;
17use tatara_rust_ast::{AstError, CompileToCrate};
18
19/// One worked example: a consumer struct/enum + the assertions that
20/// should hold after applying the derive.
21#[derive(Clone, Debug)]
22pub struct Example {
23    /// Human-readable name — used as the consumer crate suffix.
24    pub name: String,
25    /// Source of the consumer struct/enum (raw Rust). The `#[derive(<X>)]`
26    /// attribute is added by the harness; do not include it.
27    pub consumer_item: String,
28    /// `#[test]` body that runs against the consumer item. Has access to
29    /// the type via `super::*`.
30    pub assertion_body: String,
31}
32
33/// An example pack — a derive Spec + N worked examples.
34pub struct DeriveExamplePackSpec<'a, T: CompileToCrate + ?Sized> {
35    pub derive_crate_name: String,
36    /// The trait/derive identifier the consumer writes as `#[derive(...)]`.
37    pub trait_name: String,
38    /// The derive Spec under test.
39    pub spec: &'a T,
40    /// Imports the consumer needs in addition to the derive crate — e.g.
41    /// `"use my_trait::Marker;"` when the derive emits an impl whose
42    /// trait lives outside the derive crate. One per line.
43    pub extra_consumer_imports: Vec<String>,
44    /// Trait crates the consumer also needs. Path-deps in the consumer
45    /// Cargo.toml. Each tuple is `(crate_name, src_lib_rs_contents)`.
46    pub auxiliary_trait_crates: Vec<(String, String)>,
47    /// The worked examples.
48    pub examples: Vec<Example>,
49}
50
51/// Result of running an example pack against `cargo test`.
52#[derive(Debug)]
53pub struct PackRunReport {
54    pub temp_root: std::path::PathBuf,
55    pub cargo_test_succeeded: bool,
56}
57
58impl<'a, T: CompileToCrate + ?Sized> DeriveExamplePackSpec<'a, T> {
59    /// Materialize the derive crate + every auxiliary trait crate + one
60    /// consumer crate per Example under `root`. Drives `cargo test`
61    /// against the consumer; returns true on success.
62    pub fn run_under(&self, root: &Path) -> Result<PackRunReport, AstError> {
63        std::fs::create_dir_all(root)?;
64
65        // 1. Derive crate.
66        let derive_root = root.join(&self.derive_crate_name);
67        self.spec
68            .compile_to_crate(&self.derive_crate_name)?
69            .write_to(&derive_root)?;
70
71        // 2. Auxiliary trait crates.
72        for (name, lib_rs) in &self.auxiliary_trait_crates {
73            let dir = root.join(name).join("src");
74            std::fs::create_dir_all(&dir)?;
75            std::fs::write(
76                root.join(name).join("Cargo.toml"),
77                format!(
78                    r#"[package]
79name = "{name}"
80version = "0.1.0"
81edition = "2024"
82
83[lib]
84path = "src/lib.rs"
85"#
86                ),
87            )?;
88            std::fs::write(dir.join("lib.rs"), lib_rs)?;
89        }
90
91        // 3. One consumer crate with all examples folded in.
92        let consumer = root.join("consumer");
93        std::fs::create_dir_all(consumer.join("src"))?;
94        std::fs::write(consumer.join("Cargo.toml"), self.render_consumer_cargo())?;
95        std::fs::write(consumer.join("src/lib.rs"), self.render_consumer_lib())?;
96
97        // 4. Drive `cargo test`.
98        let status = Command::new("cargo")
99            .arg("test")
100            .current_dir(&consumer)
101            .status()?;
102        Ok(PackRunReport {
103            temp_root: root.to_path_buf(),
104            cargo_test_succeeded: status.success(),
105        })
106    }
107
108    fn render_consumer_cargo(&self) -> String {
109        let derive_under = self.derive_crate_name.replace('-', "_");
110        let derive_dep = format!(
111            r#"{derive_crate} = {{ path = "../{derive_crate}" }}"#,
112            derive_crate = self.derive_crate_name
113        );
114        let aux_deps = self
115            .auxiliary_trait_crates
116            .iter()
117            .map(|(n, _)| format!(r#"{n} = {{ path = "../{n}" }}"#))
118            .collect::<Vec<_>>()
119            .join("\n");
120        let _ = derive_under;
121        format!(
122            r#"[package]
123name = "consumer"
124version = "0.1.0"
125edition = "2024"
126
127[dependencies]
128{derive_dep}
129{aux_deps}
130
131[lib]
132path = "src/lib.rs"
133"#
134        )
135    }
136
137    fn render_consumer_lib(&self) -> String {
138        let derive_under = self.derive_crate_name.replace('-', "_");
139        let extra_imports = self.extra_consumer_imports.join("\n");
140        let mut items = String::new();
141        let mut tests = String::new();
142        for ex in &self.examples {
143            let ex_under = ex.name.replace('-', "_");
144            items.push_str(&format!(
145                "#[derive({trait_name})]\n{src}\n\n",
146                trait_name = self.trait_name,
147                src = ex.consumer_item
148            ));
149            tests.push_str(&format!(
150                "    #[test] fn ex_{ex_under}() {{\n{body}\n    }}\n",
151                body = ex.assertion_body
152            ));
153        }
154        format!(
155            r#"use {derive_under}::{trait_name};
156{extra_imports}
157
158{items}
159
160#[cfg(test)]
161mod tests {{
162    use super::*;
163{tests}
164}}
165"#,
166            trait_name = self.trait_name
167        )
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use tatara_rust_derive::{PerFieldDeriveSpec, PerFieldTarget};
175    use tatara_rust_ast::Ident;
176
177    fn getter_spec() -> PerFieldDeriveSpec {
178        PerFieldDeriveSpec {
179            trait_name: Ident::new("GetterPack"),
180            target: PerFieldTarget::NamedStruct,
181            trait_ref: None,
182            per_field_template:
183                "pub fn #field_name(&self) -> &#field_ty { &self.#field_name }".into(),
184            method_name_template: None,
185            impl_prelude: None,
186            skip_fields: vec![],
187            field_attribute: None,
188        }
189    }
190
191    #[test]
192    fn pack_renders_consumer_with_derives_and_assertions() {
193        let spec = getter_spec();
194        let pack = DeriveExamplePackSpec {
195            derive_crate_name: "getter-pack-derive".into(),
196            trait_name: "GetterPack".into(),
197            spec: &spec,
198            extra_consumer_imports: vec![],
199            auxiliary_trait_crates: vec![],
200            examples: vec![Example {
201                name: "two-fields".into(),
202                consumer_item: "pub struct TwoFields { pub a: i32, pub b: String }".into(),
203                assertion_body: r#"
204        let t = TwoFields { a: 1, b: "x".into() };
205        assert_eq!(*t.a(), 1);
206        assert_eq!(t.b(), "x");"#
207                    .into(),
208            }],
209        };
210        let lib = pack.render_consumer_lib();
211        assert!(lib.contains("use getter_pack_derive::GetterPack;"));
212        assert!(lib.contains("#[derive(GetterPack)]"));
213        assert!(lib.contains("pub struct TwoFields"));
214        assert!(lib.contains("fn ex_two_fields"));
215    }
216
217    #[test]
218    fn pack_cargo_lists_aux_trait_path_deps() {
219        let spec = getter_spec();
220        let pack = DeriveExamplePackSpec {
221            derive_crate_name: "x-derive".into(),
222            trait_name: "X".into(),
223            spec: &spec,
224            extra_consumer_imports: vec!["use x_trait::X;".into()],
225            auxiliary_trait_crates: vec![("x-trait".into(), "pub trait X {}".into())],
226            examples: vec![],
227        };
228        let cargo = pack.render_consumer_cargo();
229        assert!(cargo.contains(r#"x-derive = { path = "../x-derive" }"#));
230        assert!(cargo.contains(r#"x-trait = { path = "../x-trait" }"#));
231    }
232}