aprender-present-test-macros 0.30.0

Proc macros for Presentar testing framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
//! Proc macros for Presentar testing framework.
//!
//! Provides the `#[presentar_test]` attribute macro for widget and integration tests.
//!
//! # Example
//!
//! ```ignore
//! use presentar_test_macros::presentar_test;
//!
//! #[presentar_test]
//! fn test_button_renders() {
//!     let button = Button::new("Click me");
//!     let harness = Harness::new(button);
//!     harness.assert_exists("Button");
//! }
//!
//! #[presentar_test(fixture = "dashboard.tar")]
//! fn test_dashboard_layout() {
//!     // Fixture is automatically loaded
//!     harness.assert_exists("[data-testid='metric-card']");
//! }
//! ```

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
    parse::{Parse, ParseStream},
    parse_macro_input, Ident, ItemFn, LitInt, LitStr, Token,
};

/// Parsed attributes for `#[presentar_test]`.
#[derive(Default)]
struct PresentarTestAttrs {
    fixture: Option<String>,
    timeout_ms: u64,
    should_panic: bool,
    ignore: bool,
}

impl Parse for PresentarTestAttrs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut attrs = Self {
            timeout_ms: 5000,
            ..Default::default()
        };

        while !input.is_empty() {
            let ident: Ident = input.parse()?;
            let ident_str = ident.to_string();

            match ident_str.as_str() {
                "fixture" => {
                    input.parse::<Token![=]>()?;
                    let lit: LitStr = input.parse()?;
                    attrs.fixture = Some(lit.value());
                }
                "timeout" => {
                    input.parse::<Token![=]>()?;
                    let lit: LitInt = input.parse()?;
                    attrs.timeout_ms = lit.base10_parse().unwrap_or(5000);
                }
                "should_panic" => {
                    attrs.should_panic = true;
                }
                "ignore" => {
                    attrs.ignore = true;
                }
                _ => {
                    return Err(syn::Error::new(
                        ident.span(),
                        format!("unknown attribute: {ident_str}"),
                    ));
                }
            }

            // Consume optional comma
            if input.peek(Token![,]) {
                input.parse::<Token![,]>()?;
            }
        }

        Ok(attrs)
    }
}

/// Test attribute for Presentar widget and integration tests.
///
/// # Attributes
///
/// - `fixture = "path"` - Load a fixture tar file before the test
/// - `timeout = 5000` - Set test timeout in milliseconds
/// - `should_panic` - Expect the test to panic
/// - `ignore` - Skip this test by default
///
/// # Example
///
/// ```ignore
/// #[presentar_test]
/// fn test_widget() {
///     // Test code
/// }
///
/// #[presentar_test(fixture = "app.tar", timeout = 10000)]
/// fn test_with_fixture() {
///     // Test with fixture
/// }
/// ```
#[proc_macro_attribute]
pub fn presentar_test(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let attrs = parse_macro_input!(attr as PresentarTestAttrs);

    let expanded = impl_presentar_test(&input, &attrs);
    TokenStream::from(expanded)
}

fn impl_presentar_test(input: &ItemFn, attrs: &PresentarTestAttrs) -> TokenStream2 {
    let _fn_name = &input.sig.ident;
    let fn_body = &input.block;
    let fn_attrs = &input.attrs;
    let fn_vis = &input.vis;
    let fn_sig = &input.sig;

    // Generate test attributes
    let test_attr = if attrs.should_panic {
        quote! { #[test] #[should_panic] }
    } else {
        quote! { #[test] }
    };

    let ignore_attr = if attrs.ignore {
        quote! { #[ignore] }
    } else {
        quote! {}
    };

    // Generate fixture loading code if specified
    let fixture_code = if let Some(fixture_path) = &attrs.fixture {
        quote! {
            let _fixture_data = include_bytes!(#fixture_path);
            // Fixture loading would happen here
        }
    } else {
        quote! {}
    };

    // Generate timeout wrapper
    let timeout_ms = attrs.timeout_ms;
    let timeout_code = quote! {
        let _timeout_ms: u64 = #timeout_ms;
        // Timeout enforcement would happen in async context
    };

    // Generate the test function
    quote! {
        #(#fn_attrs)*
        #test_attr
        #ignore_attr
        #fn_vis #fn_sig {
            #fixture_code
            #timeout_code
            #fn_body
        }
    }
}

/// Describe a test suite with before/after hooks.
///
/// This is a function-like macro alternative to the BDD module.
///
/// # Example
///
/// ```ignore
/// describe_suite! {
///     name: "Button Widget",
///     before: || { setup(); },
///     after: || { teardown(); },
///     tests: {
///         it "renders with label" => {
///             // Test code
///         },
///         it "handles click" => {
///             // Test code
///         }
///     }
/// }
/// ```
#[proc_macro]
pub fn describe_suite(input: TokenStream) -> TokenStream {
    // Simple implementation that generates standard tests
    let _input_str = input.to_string();

    // For now, just generate a placeholder
    let expanded = quote! {
        // describe_suite macro placeholder
        // Full implementation would parse the DSL and generate test functions
    };

    TokenStream::from(expanded)
}

/// Assert that a widget matches a snapshot.
///
/// # Example
///
/// ```ignore
/// #[presentar_test]
/// fn test_button_snapshot() {
///     let button = Button::new("Submit");
///     assert_snapshot!(button, "button_submit");
/// }
/// ```
#[proc_macro]
pub fn assert_snapshot(input: TokenStream) -> TokenStream {
    let input2 = TokenStream2::from(input);

    let expanded = quote! {
        {
            let (widget, name) = (#input2);
            let snapshot = presentar_test::Snapshot::capture(&widget);
            snapshot.assert_match(name);
        }
    };

    TokenStream::from(expanded)
}

/// Define a test fixture with setup/teardown.
///
/// # Example
///
/// ```ignore
/// fixture!(
///     name = "database",
///     setup = || { create_test_db() },
///     teardown = |db| { db.drop() }
/// );
/// ```
#[proc_macro]
pub fn fixture(input: TokenStream) -> TokenStream {
    let input2 = TokenStream2::from(input);

    let expanded = quote! {
        // fixture macro placeholder
        // Would generate fixture struct with setup/teardown
        #input2
    };

    TokenStream::from(expanded)
}

// =============================================================================
// COMPUTEBLOCK ARCHITECTURAL ENFORCEMENT
// =============================================================================
//
// SPEC-024: TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS.
//
// These macros make it IMPOSSIBLE to build without tests.
// The test creates a "proof" type that the implementation requires.
// Without the test -> no proof type -> compile error.

/// Marks a test as defining an interface.
///
/// This macro generates a proof type that implementations must consume.
/// Without this test existing, implementations cannot compile.
///
/// # Example
///
/// ```ignore
/// // In tests/cpu_interface.rs
/// #[interface_test(CpuMetrics)]
/// fn test_cpu_metrics_has_frequency() {
///     let metrics = CpuMetrics::default();
///     let _freq: u64 = metrics.frequency; // Defines the interface
/// }
///
/// // In src/cpu.rs - this line requires the test to exist:
/// use crate::tests::cpu_interface::CpuMetricsInterfaceProof;
/// ```
#[proc_macro_attribute]
pub fn interface_test(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let interface_name: Ident = parse_macro_input!(attr as Ident);

    let _fn_name = &input.sig.ident;
    let fn_body = &input.block;
    let fn_attrs = &input.attrs;
    let fn_vis = &input.vis;
    let fn_sig = &input.sig;

    // Generate proof type name: CpuMetrics -> CpuMetricsInterfaceProof
    let proof_type = Ident::new(
        &format!("{interface_name}InterfaceProof"),
        interface_name.span(),
    );

    let expanded = quote! {
        /// Proof that the interface test exists.
        /// Implementation code must reference this type to compile.
        /// This enforces SPEC-024: Tests define interface.
        #[allow(dead_code)]
        pub struct #proof_type {
            _private: (),
        }

        impl #proof_type {
            /// Only callable from test modules.
            #[cfg(test)]
            pub const fn verified() -> Self {
                Self { _private: () }
            }
        }

        #(#fn_attrs)*
        #[test]
        #fn_vis #fn_sig {
            // Proof that this test defines the interface
            let _proof = #proof_type { _private: () };
            #fn_body
        }
    };

    TokenStream::from(expanded)
}

/// Requires an interface test to exist for this implementation.
///
/// Place this on impl blocks or structs that must have interface tests.
/// Without the corresponding `#[interface_test(Name)]` test, this fails to compile.
///
/// # Example
///
/// ```ignore
/// // This only compiles if tests/cpu_interface.rs has #[interface_test(CpuMetrics)]
/// #[requires_interface(CpuMetrics)]
/// impl CpuMetrics {
///     pub fn frequency(&self) -> u64 { ... }
/// }
/// ```
#[proc_macro_attribute]
pub fn requires_interface(attr: TokenStream, item: TokenStream) -> TokenStream {
    let interface_name: Ident = parse_macro_input!(attr as Ident);
    let item2 = TokenStream2::from(item);

    // Generate proof type reference
    let proof_type = Ident::new(
        &format!("{interface_name}InterfaceProof"),
        interface_name.span(),
    );

    let expanded = quote! {
        // SPEC-024 ENFORCEMENT: This code requires an interface test.
        // If you see a compile error here, you need to create:
        //   #[interface_test(#interface_name)]
        //   fn test_xxx() { ... }
        //
        // TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS.
        #[allow(dead_code)]
        const _: () = {
            // This line fails if the interface test doesn't exist
            fn _require_interface_test() {
                let _ = core::mem::size_of::<#proof_type>();
            }
        };

        #item2
    };

    TokenStream::from(expanded)
}

/// Macro for defining a ComputeBlock with mandatory test coverage.
///
/// A ComputeBlock is a self-contained unit of functionality that:
/// 1. Has a defined interface (via tests)
/// 2. Has documented behavior (via tests)
/// 3. Cannot exist without tests
///
/// # Example
///
/// ```ignore
/// // Define the block - this REQUIRES tests to exist
/// computeblock! {
///     name: CpuPanel,
///     interface: [
///         per_core_freq: Vec<u64>,
///         per_core_temp: Vec<f32>,
///     ],
///     tests: "tests/cpu_panel_interface.rs"
/// }
/// ```
#[proc_macro]
pub fn computeblock(input: TokenStream) -> TokenStream {
    let input_str = input.to_string();

    // Parse the DSL (simplified for now)
    // Full implementation would parse name, interface fields, test file path

    if !input_str.contains("name:") || !input_str.contains("tests:") {
        return TokenStream::from(quote! {
            compile_error!(
                "SPEC-024 ENFORCEMENT: computeblock! requires 'name:' and 'tests:' fields.\n\
                 TESTS DEFINE INTERFACE. IMPLEMENTATION FOLLOWS."
            );
        });
    }

    // Generate the block with enforcement
    let expanded = quote! {
        // ComputeBlock definition with enforced test coverage
        // See SPEC-024 for architecture details
    };

    TokenStream::from(expanded)
}

#[cfg(test)]
mod tests {
    // Proc macro tests run in a separate compilation unit
    // Integration tests would go in tests/ directory
}