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}