Skip to main content

presentar_test_macros/
lib.rs

1//! Proc macros for Presentar testing framework.
2//!
3//! Provides the `#[presentar_test]` attribute macro for widget and integration tests.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use presentar_test_macros::presentar_test;
9//!
10//! #[presentar_test]
11//! fn test_button_renders() {
12//!     let button = Button::new("Click me");
13//!     let harness = Harness::new(button);
14//!     harness.assert_exists("Button");
15//! }
16//!
17//! #[presentar_test(fixture = "dashboard.tar")]
18//! fn test_dashboard_layout() {
19//!     // Fixture is automatically loaded
20//!     harness.assert_exists("[data-testid='metric-card']");
21//! }
22//! ```
23
24use proc_macro::TokenStream;
25use proc_macro2::TokenStream as TokenStream2;
26use quote::quote;
27use syn::{
28    parse::{Parse, ParseStream},
29    parse_macro_input, Ident, ItemFn, LitInt, LitStr, Token,
30};
31
32/// Parsed attributes for `#[presentar_test]`.
33#[derive(Default)]
34struct PresentarTestAttrs {
35    fixture: Option<String>,
36    timeout_ms: u64,
37    should_panic: bool,
38    ignore: bool,
39}
40
41impl Parse for PresentarTestAttrs {
42    fn parse(input: ParseStream) -> syn::Result<Self> {
43        let mut attrs = Self {
44            timeout_ms: 5000,
45            ..Default::default()
46        };
47
48        while !input.is_empty() {
49            let ident: Ident = input.parse()?;
50            let ident_str = ident.to_string();
51
52            match ident_str.as_str() {
53                "fixture" => {
54                    input.parse::<Token![=]>()?;
55                    let lit: LitStr = input.parse()?;
56                    attrs.fixture = Some(lit.value());
57                }
58                "timeout" => {
59                    input.parse::<Token![=]>()?;
60                    let lit: LitInt = input.parse()?;
61                    attrs.timeout_ms = lit.base10_parse().unwrap_or(5000);
62                }
63                "should_panic" => {
64                    attrs.should_panic = true;
65                }
66                "ignore" => {
67                    attrs.ignore = true;
68                }
69                _ => {
70                    return Err(syn::Error::new(
71                        ident.span(),
72                        format!("unknown attribute: {ident_str}"),
73                    ));
74                }
75            }
76
77            // Consume optional comma
78            if input.peek(Token![,]) {
79                input.parse::<Token![,]>()?;
80            }
81        }
82
83        Ok(attrs)
84    }
85}
86
87/// Test attribute for Presentar widget and integration tests.
88///
89/// # Attributes
90///
91/// - `fixture = "path"` - Load a fixture tar file before the test
92/// - `timeout = 5000` - Set test timeout in milliseconds
93/// - `should_panic` - Expect the test to panic
94/// - `ignore` - Skip this test by default
95///
96/// # Example
97///
98/// ```ignore
99/// #[presentar_test]
100/// fn test_widget() {
101///     // Test code
102/// }
103///
104/// #[presentar_test(fixture = "app.tar", timeout = 10000)]
105/// fn test_with_fixture() {
106///     // Test with fixture
107/// }
108/// ```
109#[proc_macro_attribute]
110pub fn presentar_test(attr: TokenStream, item: TokenStream) -> TokenStream {
111    let input = parse_macro_input!(item as ItemFn);
112    let attrs = parse_macro_input!(attr as PresentarTestAttrs);
113
114    let expanded = impl_presentar_test(&input, &attrs);
115    TokenStream::from(expanded)
116}
117
118fn impl_presentar_test(input: &ItemFn, attrs: &PresentarTestAttrs) -> TokenStream2 {
119    let _fn_name = &input.sig.ident;
120    let fn_body = &input.block;
121    let fn_attrs = &input.attrs;
122    let fn_vis = &input.vis;
123    let fn_sig = &input.sig;
124
125    // Generate test attributes
126    let test_attr = if attrs.should_panic {
127        quote! { #[test] #[should_panic] }
128    } else {
129        quote! { #[test] }
130    };
131
132    let ignore_attr = if attrs.ignore {
133        quote! { #[ignore] }
134    } else {
135        quote! {}
136    };
137
138    // Generate fixture loading code if specified
139    let fixture_code = if let Some(fixture_path) = &attrs.fixture {
140        quote! {
141            let _fixture_data = include_bytes!(#fixture_path);
142            // Fixture loading would happen here
143        }
144    } else {
145        quote! {}
146    };
147
148    // Generate timeout wrapper
149    let timeout_ms = attrs.timeout_ms;
150    let timeout_code = quote! {
151        let _timeout_ms: u64 = #timeout_ms;
152        // Timeout enforcement would happen in async context
153    };
154
155    // Generate the test function
156    quote! {
157        #(#fn_attrs)*
158        #test_attr
159        #ignore_attr
160        #fn_vis #fn_sig {
161            #fixture_code
162            #timeout_code
163            #fn_body
164        }
165    }
166}
167
168/// Describe a test suite with before/after hooks.
169///
170/// This is a function-like macro alternative to the BDD module.
171///
172/// # Example
173///
174/// ```ignore
175/// describe_suite! {
176///     name: "Button Widget",
177///     before: || { setup(); },
178///     after: || { teardown(); },
179///     tests: {
180///         it "renders with label" => {
181///             // Test code
182///         },
183///         it "handles click" => {
184///             // Test code
185///         }
186///     }
187/// }
188/// ```
189#[proc_macro]
190pub fn describe_suite(input: TokenStream) -> TokenStream {
191    // Simple implementation that generates standard tests
192    let _input_str = input.to_string();
193
194    // For now, just generate a placeholder
195    let expanded = quote! {
196        // describe_suite macro placeholder
197        // Full implementation would parse the DSL and generate test functions
198    };
199
200    TokenStream::from(expanded)
201}
202
203/// Assert that a widget matches a snapshot.
204///
205/// # Example
206///
207/// ```ignore
208/// #[presentar_test]
209/// fn test_button_snapshot() {
210///     let button = Button::new("Submit");
211///     assert_snapshot!(button, "button_submit");
212/// }
213/// ```
214#[proc_macro]
215pub fn assert_snapshot(input: TokenStream) -> TokenStream {
216    let input2 = TokenStream2::from(input);
217
218    let expanded = quote! {
219        {
220            let (widget, name) = (#input2);
221            let snapshot = presentar_test::Snapshot::capture(&widget);
222            snapshot.assert_match(name);
223        }
224    };
225
226    TokenStream::from(expanded)
227}
228
229/// Define a test fixture with setup/teardown.
230///
231/// # Example
232///
233/// ```ignore
234/// fixture!(
235///     name = "database",
236///     setup = || { create_test_db() },
237///     teardown = |db| { db.drop() }
238/// );
239/// ```
240#[proc_macro]
241pub fn fixture(input: TokenStream) -> TokenStream {
242    let input2 = TokenStream2::from(input);
243
244    let expanded = quote! {
245        // fixture macro placeholder
246        // Would generate fixture struct with setup/teardown
247        #input2
248    };
249
250    TokenStream::from(expanded)
251}
252
253// =============================================================================
254// COMPUTEBLOCK ARCHITECTURAL ENFORCEMENT
255// =============================================================================
256//
257// SPEC-024: TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS.
258//
259// These macros make it IMPOSSIBLE to build without tests.
260// The test creates a "proof" type that the implementation requires.
261// Without the test -> no proof type -> compile error.
262
263/// Marks a test as defining an interface.
264///
265/// This macro generates a proof type that implementations must consume.
266/// Without this test existing, implementations cannot compile.
267///
268/// # Example
269///
270/// ```ignore
271/// // In tests/cpu_interface.rs
272/// #[interface_test(CpuMetrics)]
273/// fn test_cpu_metrics_has_frequency() {
274///     let metrics = CpuMetrics::default();
275///     let _freq: u64 = metrics.frequency; // Defines the interface
276/// }
277///
278/// // In src/cpu.rs - this line requires the test to exist:
279/// use crate::tests::cpu_interface::CpuMetricsInterfaceProof;
280/// ```
281#[proc_macro_attribute]
282pub fn interface_test(attr: TokenStream, item: TokenStream) -> TokenStream {
283    let input = parse_macro_input!(item as ItemFn);
284    let interface_name: Ident = parse_macro_input!(attr as Ident);
285
286    let _fn_name = &input.sig.ident;
287    let fn_body = &input.block;
288    let fn_attrs = &input.attrs;
289    let fn_vis = &input.vis;
290    let fn_sig = &input.sig;
291
292    // Generate proof type name: CpuMetrics -> CpuMetricsInterfaceProof
293    let proof_type = Ident::new(
294        &format!("{interface_name}InterfaceProof"),
295        interface_name.span(),
296    );
297
298    let expanded = quote! {
299        /// Proof that the interface test exists.
300        /// Implementation code must reference this type to compile.
301        /// This enforces SPEC-024: Tests define interface.
302        #[allow(dead_code)]
303        pub struct #proof_type {
304            _private: (),
305        }
306
307        impl #proof_type {
308            /// Only callable from test modules.
309            #[cfg(test)]
310            pub const fn verified() -> Self {
311                Self { _private: () }
312            }
313        }
314
315        #(#fn_attrs)*
316        #[test]
317        #fn_vis #fn_sig {
318            // Proof that this test defines the interface
319            let _proof = #proof_type { _private: () };
320            #fn_body
321        }
322    };
323
324    TokenStream::from(expanded)
325}
326
327/// Requires an interface test to exist for this implementation.
328///
329/// Place this on impl blocks or structs that must have interface tests.
330/// Without the corresponding `#[interface_test(Name)]` test, this fails to compile.
331///
332/// # Example
333///
334/// ```ignore
335/// // This only compiles if tests/cpu_interface.rs has #[interface_test(CpuMetrics)]
336/// #[requires_interface(CpuMetrics)]
337/// impl CpuMetrics {
338///     pub fn frequency(&self) -> u64 { ... }
339/// }
340/// ```
341#[proc_macro_attribute]
342pub fn requires_interface(attr: TokenStream, item: TokenStream) -> TokenStream {
343    let interface_name: Ident = parse_macro_input!(attr as Ident);
344    let item2 = TokenStream2::from(item);
345
346    // Generate proof type reference
347    let proof_type = Ident::new(
348        &format!("{interface_name}InterfaceProof"),
349        interface_name.span(),
350    );
351
352    let expanded = quote! {
353        // SPEC-024 ENFORCEMENT: This code requires an interface test.
354        // If you see a compile error here, you need to create:
355        //   #[interface_test(#interface_name)]
356        //   fn test_xxx() { ... }
357        //
358        // TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS.
359        #[allow(dead_code)]
360        const _: () = {
361            // This line fails if the interface test doesn't exist
362            fn _require_interface_test() {
363                let _ = core::mem::size_of::<#proof_type>();
364            }
365        };
366
367        #item2
368    };
369
370    TokenStream::from(expanded)
371}
372
373/// Macro for defining a ComputeBlock with mandatory test coverage.
374///
375/// A ComputeBlock is a self-contained unit of functionality that:
376/// 1. Has a defined interface (via tests)
377/// 2. Has documented behavior (via tests)
378/// 3. Cannot exist without tests
379///
380/// # Example
381///
382/// ```ignore
383/// // Define the block - this REQUIRES tests to exist
384/// computeblock! {
385///     name: CpuPanel,
386///     interface: [
387///         per_core_freq: Vec<u64>,
388///         per_core_temp: Vec<f32>,
389///     ],
390///     tests: "tests/cpu_panel_interface.rs"
391/// }
392/// ```
393#[proc_macro]
394pub fn computeblock(input: TokenStream) -> TokenStream {
395    let input_str = input.to_string();
396
397    // Parse the DSL (simplified for now)
398    // Full implementation would parse name, interface fields, test file path
399
400    if !input_str.contains("name:") || !input_str.contains("tests:") {
401        return TokenStream::from(quote! {
402            compile_error!(
403                "SPEC-024 ENFORCEMENT: computeblock! requires 'name:' and 'tests:' fields.\n\
404                 TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS."
405            );
406        });
407    }
408
409    // Generate the block with enforcement
410    let expanded = quote! {
411        // ComputeBlock definition with enforced test coverage
412        // See SPEC-024 for architecture details
413    };
414
415    TokenStream::from(expanded)
416}
417
418#[cfg(test)]
419mod tests {
420    // Proc macro tests run in a separate compilation unit
421    // Integration tests would go in tests/ directory
422}