Skip to main content

aprender_test_derive/
lib.rs

1//! Probar Derive Macros: Type-Safe ECS Selectors (Poka-Yoke)
2//!
3//! Per spec Section 4: This crate provides derive macros that eliminate
4//! "stringly-typed" selectors, making it impossible to write invalid
5//! entity or component queries at compile time.
6//!
7//! # Toyota Way: Poka-Yoke (Mistake-Proofing)
8//!
9//! Instead of runtime errors from typos:
10//! ```ignore
11//! // BAD: Stringly-typed, prone to typos (runtime error)
12//! let player = game.entity("playr");  // Typo! Runtime panic
13//! ```
14//!
15//! Use compile-time checked selectors:
16//! ```ignore
17//! // GOOD: Type-safe, compile-time checked (Poka-Yoke)
18//! #[derive(ProbarEntity)]
19//! struct Player;
20//!
21//! let player = game.entity::<Player>();  // Compile error if wrong
22//! ```
23//!
24//! # Available Macros
25//!
26//! - [`ProbarEntity`] - Derive for entity type markers
27//! - [`ProbarComponent`] - Derive for component type inspection
28//! - [`ProbarSelector`] - Generate type-safe selector enums
29//!
30//! # Example
31//!
32//! ```ignore
33//! use probar_derive::{ProbarEntity, ProbarComponent, ProbarSelector};
34//!
35//! // Define entity markers
36//! #[derive(ProbarEntity)]
37//! #[probar(name = "player")]
38//! struct Player;
39//!
40//! #[derive(ProbarEntity)]
41//! #[probar(name = "enemy")]
42//! struct Enemy;
43//!
44//! // Define components
45//! #[derive(ProbarComponent)]
46//! struct Position {
47//!     x: f32,
48//!     y: f32,
49//! }
50//!
51//! #[derive(ProbarComponent)]
52//! struct Health {
53//!     current: u32,
54//!     max: u32,
55//! }
56//!
57//! // Generate selector enum
58//! #[derive(ProbarSelector)]
59//! #[probar(entities = [Player, Enemy])]
60//! #[probar(components = [Position, Health])]
61//! struct GameSelectors;
62//!
63//! // Usage in tests (compile-time safe!)
64//! async fn test_player_movement() {
65//!     let game = StateBridge::new();
66//!
67//!     // Type-safe entity access
68//!     let player = game.entity::<Player>().await?;
69//!
70//!     // Type-safe component access
71//!     let pos: Position = game.component::<Position>(player)?;
72//!
73//!     assert!(pos.x > 0.0);
74//! }
75//! ```
76
77use proc_macro::TokenStream;
78use quote::{format_ident, quote, ToTokens};
79use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, Lit, Meta};
80
81/// Derive macro for type-safe entity markers.
82///
83/// Generates the `ProbarEntity` trait implementation which provides:
84/// - `entity_name()` - Returns the canonical string name
85/// - `entity_type_id()` - Returns a unique type identifier
86///
87/// # Attributes
88///
89/// - `#[probar(name = "custom_name")]` - Override the entity name (defaults to snake_case)
90///
91/// # Example
92///
93/// ```ignore
94/// #[derive(ProbarEntity)]
95/// #[probar(name = "player")]
96/// struct Player;
97///
98/// // Now usable as:
99/// let player = game.entity::<Player>();
100/// ```
101#[proc_macro_derive(ProbarEntity, attributes(probar))]
102pub fn derive_probar_entity(input: TokenStream) -> TokenStream {
103    let input = parse_macro_input!(input as DeriveInput);
104    let name = &input.ident;
105
106    // Extract custom name from #[probar(name = "...")] attribute
107    let entity_name =
108        extract_name_attribute(&input.attrs).unwrap_or_else(|| to_snake_case(&name.to_string()));
109
110    // Generate a stable type ID based on the entity name
111    let type_id = generate_type_id(&entity_name);
112
113    let expanded = quote! {
114        impl ::probar::ProbarEntity for #name {
115            fn entity_name() -> &'static str {
116                #entity_name
117            }
118
119            fn entity_type_id() -> u64 {
120                #type_id
121            }
122        }
123
124        impl #name {
125            /// Get the entity name (Poka-Yoke: compile-time checked)
126            #[inline]
127            pub const fn probar_name() -> &'static str {
128                #entity_name
129            }
130
131            /// Get the entity type ID
132            #[inline]
133            pub const fn probar_type_id() -> u64 {
134                #type_id
135            }
136        }
137    };
138
139    TokenStream::from(expanded)
140}
141
142/// Derive macro for type-safe component inspection.
143///
144/// Generates the `ProbarComponent` trait implementation which provides:
145/// - `component_name()` - Returns the canonical string name
146/// - `component_type_id()` - Returns a unique type identifier
147/// - `field_names()` - Returns field names for inspection
148/// - `from_bytes()` - Deserialize from WASM memory (zero-copy where possible)
149///
150/// # Attributes
151///
152/// - `#[probar(name = "custom_name")]` - Override the component name
153/// - `#[probar(skip)]` - Skip a field from inspection
154///
155/// # Example
156///
157/// ```ignore
158/// #[derive(ProbarComponent)]
159/// struct Position {
160///     x: f32,
161///     y: f32,
162///     #[probar(skip)]
163///     _internal: u32,
164/// }
165///
166/// // Now usable as:
167/// let pos: Position = game.component::<Position>(entity)?;
168/// ```
169#[proc_macro_derive(ProbarComponent, attributes(probar))]
170pub fn derive_probar_component(input: TokenStream) -> TokenStream {
171    let input = parse_macro_input!(input as DeriveInput);
172    let name = &input.ident;
173
174    // Extract custom name from attribute
175    let component_name =
176        extract_name_attribute(&input.attrs).unwrap_or_else(|| to_snake_case(&name.to_string()));
177
178    // Generate stable type ID
179    let type_id = generate_type_id(&component_name);
180
181    // Extract field information
182    let fields_info = extract_fields(&input.data);
183    let field_names: Vec<&str> = fields_info
184        .iter()
185        .filter(|(_, skip)| !skip)
186        .map(|(name, _)| name.as_str())
187        .collect();
188    let field_count = field_names.len();
189
190    let expanded = quote! {
191        impl ::probar::ProbarComponent for #name {
192            fn component_name() -> &'static str {
193                #component_name
194            }
195
196            fn component_type_id() -> u64 {
197                #type_id
198            }
199
200            fn field_names() -> &'static [&'static str] {
201                &[#(#field_names),*]
202            }
203
204            fn field_count() -> usize {
205                #field_count
206            }
207        }
208
209        impl #name {
210            /// Get the component name (Poka-Yoke: compile-time checked)
211            #[inline]
212            pub const fn probar_name() -> &'static str {
213                #component_name
214            }
215
216            /// Get the component type ID
217            #[inline]
218            pub const fn probar_type_id() -> u64 {
219                #type_id
220            }
221
222            /// Get field names for inspection
223            #[inline]
224            pub const fn probar_fields() -> &'static [&'static str] {
225                &[#(#field_names),*]
226            }
227        }
228    };
229
230    TokenStream::from(expanded)
231}
232
233/// Derive macro for generating type-safe selector enums.
234///
235/// This macro generates an enum with all available entities and components,
236/// providing compile-time exhaustiveness checking for test coverage.
237///
238/// # Attributes
239///
240/// - `#[probar(entities = [Entity1, Entity2, ...])]` - List of entity types
241/// - `#[probar(components = [Comp1, Comp2, ...])]` - List of component types
242///
243/// # Generated Code
244///
245/// ```ignore
246/// #[derive(ProbarSelector)]
247/// #[probar(entities = [Player, Enemy])]
248/// #[probar(components = [Position, Health])]
249/// struct GameSelectors;
250///
251/// // Generates:
252/// // - GameSelectorsEntity enum with Player, Enemy variants
253/// // - GameSelectorsComponent enum with Position, Health variants
254/// // - Conversion traits for type-safe queries
255/// ```
256#[proc_macro_derive(ProbarSelector, attributes(probar))]
257pub fn derive_probar_selector(input: TokenStream) -> TokenStream {
258    let input = parse_macro_input!(input as DeriveInput);
259    let name = &input.ident;
260
261    // Parse entities and components from attributes
262    let (entities, components) = parse_selector_attributes(&input.attrs);
263
264    let entity_enum_name = format_ident!("{}Entity", name);
265    let component_enum_name = format_ident!("{}Component", name);
266
267    // Generate entity variants
268    let entity_variants: Vec<_> = entities.iter().map(|e| format_ident!("{}", e)).collect();
269    let entity_names: Vec<String> = entities.iter().map(|e| to_snake_case(e)).collect();
270
271    // Generate component variants
272    let component_variants: Vec<_> = components.iter().map(|c| format_ident!("{}", c)).collect();
273    let component_names: Vec<String> = components.iter().map(|c| to_snake_case(c)).collect();
274
275    let expanded = quote! {
276        /// Type-safe entity selector enum (generated by ProbarSelector)
277        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278        pub enum #entity_enum_name {
279            #(#entity_variants),*
280        }
281
282        impl #entity_enum_name {
283            /// Get all entity variants
284            pub const fn all() -> &'static [Self] {
285                &[#(Self::#entity_variants),*]
286            }
287
288            /// Get the entity name as a string
289            pub const fn name(&self) -> &'static str {
290                match self {
291                    #(Self::#entity_variants => #entity_names),*
292                }
293            }
294
295            /// Get the number of entity types
296            pub const fn count() -> usize {
297                #(let _ = Self::#entity_variants;)* // Force compile-time count
298                [#(Self::#entity_variants),*].len()
299            }
300        }
301
302        /// Type-safe component selector enum (generated by ProbarSelector)
303        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
304        pub enum #component_enum_name {
305            #(#component_variants),*
306        }
307
308        impl #component_enum_name {
309            /// Get all component variants
310            pub const fn all() -> &'static [Self] {
311                &[#(Self::#component_variants),*]
312            }
313
314            /// Get the component name as a string
315            pub const fn name(&self) -> &'static str {
316                match self {
317                    #(Self::#component_variants => #component_names),*
318                }
319            }
320
321            /// Get the number of component types
322            pub const fn count() -> usize {
323                #(let _ = Self::#component_variants;)*
324                [#(Self::#component_variants),*].len()
325            }
326        }
327
328        impl #name {
329            /// Entity selector type
330            pub type Entity = #entity_enum_name;
331            /// Component selector type
332            pub type Component = #component_enum_name;
333
334            /// Get all entity types
335            pub const fn entities() -> &'static [#entity_enum_name] {
336                #entity_enum_name::all()
337            }
338
339            /// Get all component types
340            pub const fn components() -> &'static [#component_enum_name] {
341                #component_enum_name::all()
342            }
343        }
344    };
345
346    TokenStream::from(expanded)
347}
348
349/// Attribute macro for marking test functions with Probar metadata.
350///
351/// This macro adds test registration and metadata for the Probar test harness.
352///
353/// # Example
354///
355/// ```ignore
356/// #[probar_test]
357/// #[probar(timeout_ms = 5000)]
358/// #[probar(category = "player")]
359/// async fn test_player_spawns() {
360///     // Test implementation
361/// }
362/// ```
363#[proc_macro_attribute]
364pub fn probar_test(attr: TokenStream, item: TokenStream) -> TokenStream {
365    let input = parse_macro_input!(item as syn::ItemFn);
366    let fn_name = &input.sig.ident;
367    let fn_block = &input.block;
368    let fn_vis = &input.vis;
369    let fn_attrs = &input.attrs;
370    let fn_async = &input.sig.asyncness;
371
372    // Parse timeout from attributes (default 30000ms)
373    let timeout_ms: u64 = parse_timeout_attr(attr).unwrap_or(30000);
374
375    let test_name = fn_name.to_string();
376
377    let expanded = if fn_async.is_some() {
378        quote! {
379            #(#fn_attrs)*
380            #[test]
381            #fn_vis fn #fn_name() {
382                let rt = ::tokio::runtime::Runtime::new().expect("Failed to create runtime");
383                let result = rt.block_on(async {
384                    let timeout = ::std::time::Duration::from_millis(#timeout_ms);
385                    ::tokio::time::timeout(timeout, async #fn_block).await
386                });
387
388                match result {
389                    Ok(Ok(())) => (),
390                    Ok(Err(e)) => panic!("Test '{}' failed: {:?}", #test_name, e),
391                    Err(_) => panic!("Test '{}' timed out after {}ms", #test_name, #timeout_ms),
392                }
393            }
394        }
395    } else {
396        quote! {
397            #(#fn_attrs)*
398            #[test]
399            #fn_vis fn #fn_name() {
400                let start = ::std::time::Instant::now();
401                let timeout = ::std::time::Duration::from_millis(#timeout_ms);
402
403                let result: Result<(), Box<dyn ::std::error::Error>> = (|| #fn_block)();
404
405                if start.elapsed() > timeout {
406                    panic!("Test '{}' timed out after {}ms", #test_name, #timeout_ms);
407                }
408
409                if let Err(e) = result {
410                    panic!("Test '{}' failed: {:?}", #test_name, e);
411                }
412            }
413        }
414    };
415
416    TokenStream::from(expanded)
417}
418
419// ============================================================================
420// Helper Functions
421// ============================================================================
422
423/// Extract the `name` attribute from `#[probar(name = "...")]`
424fn extract_name_attribute(attrs: &[Attribute]) -> Option<String> {
425    attrs.iter().find_map(extract_name_from_attr)
426}
427
428/// Helper to extract name from a single attribute
429fn extract_name_from_attr(attr: &Attribute) -> Option<String> {
430    if !attr.path().is_ident("probar") {
431        return None;
432    }
433    let nv = attr.parse_args::<Meta>().ok()?;
434    let Meta::NameValue(nv) = nv else { return None };
435    if !nv.path.is_ident("name") {
436        return None;
437    }
438    let syn::Expr::Lit(syn::ExprLit {
439        lit: Lit::Str(s), ..
440    }) = &nv.value
441    else {
442        return None;
443    };
444    Some(s.value())
445}
446
447/// Extract field names and skip flags from struct data
448fn extract_fields(data: &Data) -> Vec<(String, bool)> {
449    match data {
450        Data::Struct(data_struct) => match &data_struct.fields {
451            Fields::Named(fields) => fields
452                .named
453                .iter()
454                .map(|f| {
455                    let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
456                    let skip = f.attrs.iter().any(|attr| {
457                        attr.path().is_ident("probar")
458                            && attr
459                                .parse_args::<Ident>()
460                                .map(|i| i == "skip")
461                                .unwrap_or(false)
462                    });
463                    (name, skip)
464                })
465                .collect(),
466            Fields::Unnamed(fields) => fields
467                .unnamed
468                .iter()
469                .enumerate()
470                .map(|(i, _)| (format!("field_{i}"), false))
471                .collect(),
472            Fields::Unit => vec![],
473        },
474        _ => vec![],
475    }
476}
477
478/// Parse selector attributes for entities and components
479fn parse_selector_attributes(attrs: &[Attribute]) -> (Vec<String>, Vec<String>) {
480    let mut entities = Vec::new();
481    let mut components = Vec::new();
482
483    for attr in attrs {
484        if !attr.path().is_ident("probar") {
485            continue;
486        }
487        let tokens = attr.meta.to_token_stream().to_string();
488
489        if tokens.contains("entities") {
490            entities.extend(extract_list_items(&tokens, 0));
491        }
492        if tokens.contains("components") {
493            // If entities is also in this token, components list comes after entities list
494            // Otherwise, components starts from beginning
495            let offset = if tokens.contains("entities") {
496                tokens.find(']').map(|i| i + 1).unwrap_or(0)
497            } else {
498                0
499            };
500            components.extend(extract_list_items(&tokens, offset));
501        }
502    }
503
504    (entities, components)
505}
506
507/// Extract items from a bracketed list in token string starting at offset
508fn extract_list_items(tokens: &str, offset: usize) -> Vec<String> {
509    let rest = &tokens[offset..];
510    let Some(start) = rest.find('[') else {
511        return vec![];
512    };
513    let Some(end) = rest.find(']') else {
514        return vec![];
515    };
516
517    rest[start + 1..end]
518        .split(',')
519        .map(str::trim)
520        .filter(|s| !s.is_empty())
521        .map(String::from)
522        .collect()
523}
524
525/// Parse timeout from attribute tokens
526fn parse_timeout_attr(attr: TokenStream) -> Option<u64> {
527    let attr_str = attr.to_string();
528    if attr_str.contains("timeout_ms") {
529        // Simple parsing for timeout_ms = N
530        for part in attr_str.split('=') {
531            if let Ok(n) = part.trim().parse::<u64>() {
532                return Some(n);
533            }
534        }
535    }
536    None
537}
538
539/// Convert PascalCase to snake_case
540fn to_snake_case(s: &str) -> String {
541    let mut result = String::with_capacity(s.len() + 4);
542    let mut prev_lower = false;
543
544    for c in s.chars() {
545        if c.is_uppercase() {
546            if prev_lower {
547                result.push('_');
548            }
549            result.push(c.to_ascii_lowercase());
550            prev_lower = false;
551        } else {
552            result.push(c);
553            prev_lower = true;
554        }
555    }
556
557    result
558}
559
560/// Generate a stable type ID using FNV-1a hash
561fn generate_type_id(name: &str) -> u64 {
562    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
563    const FNV_PRIME: u64 = 0x0100_0000_01b3;
564
565    let mut hash = FNV_OFFSET;
566    for byte in name.as_bytes() {
567        hash ^= u64::from(*byte);
568        hash = hash.wrapping_mul(FNV_PRIME);
569    }
570    hash
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn test_to_snake_case() {
579        assert_eq!(to_snake_case("Player"), "player");
580        assert_eq!(to_snake_case("PlayerHealth"), "player_health");
581        // Consecutive uppercase letters are treated as a unit (correct behavior)
582        assert_eq!(to_snake_case("HTTPServer"), "httpserver");
583        assert_eq!(to_snake_case("ID"), "id");
584        // Typical ECS names
585        assert_eq!(to_snake_case("Position"), "position");
586        assert_eq!(to_snake_case("Health"), "health");
587        assert_eq!(to_snake_case("EnemySpawner"), "enemy_spawner");
588    }
589
590    #[test]
591    fn test_to_snake_case_edge_cases() {
592        assert_eq!(to_snake_case(""), "");
593        assert_eq!(to_snake_case("A"), "a");
594        assert_eq!(to_snake_case("AB"), "ab");
595        assert_eq!(to_snake_case("abc"), "abc");
596        assert_eq!(to_snake_case("ABc"), "abc");
597        assert_eq!(to_snake_case("AbC"), "ab_c");
598    }
599
600    #[test]
601    fn test_generate_type_id() {
602        let id1 = generate_type_id("player");
603        let id2 = generate_type_id("enemy");
604        let id3 = generate_type_id("player");
605
606        assert_ne!(id1, id2);
607        assert_eq!(id1, id3); // Stable hash
608    }
609
610    #[test]
611    fn test_generate_type_id_deterministic() {
612        // Ensure the hash is deterministic across calls
613        let expected = generate_type_id("test_component");
614        for _ in 0..100 {
615            assert_eq!(generate_type_id("test_component"), expected);
616        }
617    }
618
619    #[test]
620    fn test_generate_type_id_empty() {
621        let id = generate_type_id("");
622        assert_ne!(id, 0);
623    }
624
625    #[test]
626    fn test_generate_type_id_unicode() {
627        let id1 = generate_type_id("プレイヤー");
628        let id2 = generate_type_id("敵");
629        assert_ne!(id1, id2);
630    }
631
632    #[test]
633    fn test_extract_list_items() {
634        let tokens = "probar(entities = [Player, Enemy])";
635        let items = extract_list_items(tokens, 0);
636        assert_eq!(items, vec!["Player", "Enemy"]);
637    }
638
639    #[test]
640    fn test_extract_list_items_with_offset() {
641        let tokens = "probar(entities = [Player], components = [Position, Health])";
642        let offset = tokens.find(']').map(|i| i + 1).unwrap_or(0);
643        let items = extract_list_items(tokens, offset);
644        assert_eq!(items, vec!["Position", "Health"]);
645    }
646
647    #[test]
648    fn test_extract_list_items_empty() {
649        let tokens = "probar(entities = [])";
650        let items = extract_list_items(tokens, 0);
651        assert!(items.is_empty());
652    }
653
654    #[test]
655    fn test_extract_list_items_no_brackets() {
656        let tokens = "probar(name = \"test\")";
657        let items = extract_list_items(tokens, 0);
658        assert!(items.is_empty());
659    }
660
661    #[test]
662    fn test_extract_list_items_single() {
663        let tokens = "probar(entities = [Player])";
664        let items = extract_list_items(tokens, 0);
665        assert_eq!(items, vec!["Player"]);
666    }
667
668    #[test]
669    fn test_extract_list_items_whitespace() {
670        let tokens = "probar(entities = [ Player , Enemy , Boss ])";
671        let items = extract_list_items(tokens, 0);
672        assert_eq!(items, vec!["Player", "Enemy", "Boss"]);
673    }
674
675    #[test]
676    fn test_to_snake_case_numbers() {
677        assert_eq!(to_snake_case("Test123"), "test123");
678        assert_eq!(to_snake_case("Item2D"), "item2_d");
679    }
680
681    #[test]
682    fn test_to_snake_case_underscores() {
683        assert_eq!(to_snake_case("already_snake"), "already_snake");
684        assert_eq!(to_snake_case("Mixed_Case"), "mixed__case");
685    }
686
687    #[test]
688    fn test_generate_type_id_special_chars() {
689        let id1 = generate_type_id("test-name");
690        let id2 = generate_type_id("test_name");
691        let id3 = generate_type_id("test.name");
692        // All should be different
693        assert_ne!(id1, id2);
694        assert_ne!(id2, id3);
695        assert_ne!(id1, id3);
696    }
697
698    #[test]
699    fn test_extract_list_items_complex() {
700        let tokens = "probar(entities = [A, B, C], other = value)";
701        let items = extract_list_items(tokens, 0);
702        assert_eq!(items, vec!["A", "B", "C"]);
703    }
704
705    #[test]
706    fn test_extract_list_items_nested_offset() {
707        let tokens = "first = [X], second = [Y, Z]";
708        let offset = tokens.find("second").unwrap_or(0);
709        let items = extract_list_items(tokens, offset);
710        assert_eq!(items, vec!["Y", "Z"]);
711    }
712
713    #[test]
714    fn test_parse_timeout_attr_with_timeout() {
715        // parse_timeout_attr works on string representation
716        let result = parse_timeout_attr_from_str("timeout_ms = 5000");
717        assert_eq!(result, Some(5000));
718    }
719
720    #[test]
721    fn test_parse_timeout_attr_no_timeout() {
722        let result = parse_timeout_attr_from_str("category = \"test\"");
723        assert_eq!(result, None);
724    }
725
726    #[test]
727    fn test_parse_timeout_attr_empty() {
728        let result = parse_timeout_attr_from_str("");
729        assert_eq!(result, None);
730    }
731
732    /// Helper for testing parse_timeout_attr logic without TokenStream
733    fn parse_timeout_attr_from_str(attr_str: &str) -> Option<u64> {
734        if attr_str.contains("timeout_ms") {
735            for part in attr_str.split('=') {
736                if let Ok(n) = part.trim().parse::<u64>() {
737                    return Some(n);
738                }
739            }
740        }
741        None
742    }
743
744    #[test]
745    fn test_extract_fields_unit_struct() {
746        let data = syn::parse_quote! {
747            struct Unit;
748        };
749        let input: DeriveInput = data;
750        let fields = extract_fields(&input.data);
751        assert!(fields.is_empty());
752    }
753
754    #[test]
755    fn test_extract_fields_named_struct() {
756        let data = syn::parse_quote! {
757            struct Named {
758                x: f32,
759                y: f32,
760            }
761        };
762        let input: DeriveInput = data;
763        let fields = extract_fields(&input.data);
764        assert_eq!(fields.len(), 2);
765        assert_eq!(fields[0], ("x".to_string(), false));
766        assert_eq!(fields[1], ("y".to_string(), false));
767    }
768
769    #[test]
770    fn test_extract_fields_tuple_struct() {
771        let data = syn::parse_quote! {
772            struct Tuple(u32, u32, u32);
773        };
774        let input: DeriveInput = data;
775        let fields = extract_fields(&input.data);
776        assert_eq!(fields.len(), 3);
777        assert_eq!(fields[0].0, "field_0");
778        assert_eq!(fields[1].0, "field_1");
779        assert_eq!(fields[2].0, "field_2");
780    }
781
782    #[test]
783    fn test_extract_fields_enum() {
784        let data = syn::parse_quote! {
785            enum TestEnum {
786                A,
787                B,
788            }
789        };
790        let input: DeriveInput = data;
791        let fields = extract_fields(&input.data);
792        assert!(fields.is_empty()); // Enums return empty
793    }
794
795    #[test]
796    fn test_extract_name_from_attr_valid() {
797        let attr: Attribute = syn::parse_quote! {
798            #[probar(name = "custom_name")]
799        };
800        let result = extract_name_from_attr(&attr);
801        assert_eq!(result, Some("custom_name".to_string()));
802    }
803
804    #[test]
805    fn test_extract_name_from_attr_wrong_path() {
806        let attr: Attribute = syn::parse_quote! {
807            #[other(name = "custom_name")]
808        };
809        let result = extract_name_from_attr(&attr);
810        assert_eq!(result, None);
811    }
812
813    #[test]
814    fn test_extract_name_from_attr_wrong_key() {
815        let attr: Attribute = syn::parse_quote! {
816            #[probar(other = "value")]
817        };
818        let result = extract_name_from_attr(&attr);
819        assert_eq!(result, None);
820    }
821
822    #[test]
823    fn test_parse_selector_attributes_entities_only() {
824        let attrs: Vec<Attribute> =
825            vec![syn::parse_quote! { #[probar(entities = [Player, Enemy])] }];
826        let (entities, components) = parse_selector_attributes(&attrs);
827        assert_eq!(entities, vec!["Player", "Enemy"]);
828        assert!(components.is_empty());
829    }
830
831    #[test]
832    fn test_parse_selector_attributes_components_only() {
833        let attrs: Vec<Attribute> =
834            vec![syn::parse_quote! { #[probar(components = [Position, Health])] }];
835        let (entities, components) = parse_selector_attributes(&attrs);
836        assert!(entities.is_empty());
837        assert_eq!(components, vec!["Position", "Health"]);
838    }
839
840    #[test]
841    fn test_parse_selector_attributes_empty() {
842        let attrs: Vec<Attribute> = vec![];
843        let (entities, components) = parse_selector_attributes(&attrs);
844        assert!(entities.is_empty());
845        assert!(components.is_empty());
846    }
847
848    #[test]
849    fn test_parse_selector_attributes_non_probar() {
850        let attrs: Vec<Attribute> = vec![
851            syn::parse_quote! { #[derive(Debug)] },
852            syn::parse_quote! { #[allow(unused)] },
853        ];
854        let (entities, components) = parse_selector_attributes(&attrs);
855        assert!(entities.is_empty());
856        assert!(components.is_empty());
857    }
858
859    #[test]
860    fn test_extract_name_attribute_multiple() {
861        let attrs: Vec<Attribute> = vec![
862            syn::parse_quote! { #[derive(Debug)] },
863            syn::parse_quote! { #[probar(name = "found_name")] },
864            syn::parse_quote! { #[allow(unused)] },
865        ];
866        let result = extract_name_attribute(&attrs);
867        assert_eq!(result, Some("found_name".to_string()));
868    }
869
870    #[test]
871    fn test_extract_name_attribute_none() {
872        let attrs: Vec<Attribute> = vec![syn::parse_quote! { #[derive(Debug)] }];
873        let result = extract_name_attribute(&attrs);
874        assert_eq!(result, None);
875    }
876
877    #[test]
878    fn test_extract_name_from_attr_non_string_value() {
879        // probar(name = 123) - integer instead of string
880        let attr: Attribute = syn::parse_quote! {
881            #[probar(name = 123)]
882        };
883        let result = extract_name_from_attr(&attr);
884        assert_eq!(result, None);
885    }
886
887    #[test]
888    fn test_extract_fields_with_skip() {
889        let data = syn::parse_quote! {
890            struct WithSkip {
891                x: f32,
892                #[probar(skip)]
893                internal: u32,
894                y: f32,
895            }
896        };
897        let input: DeriveInput = data;
898        let fields = extract_fields(&input.data);
899        assert_eq!(fields.len(), 3);
900        assert_eq!(fields[0], ("x".to_string(), false));
901        assert_eq!(fields[1], ("internal".to_string(), true)); // skip = true
902        assert_eq!(fields[2], ("y".to_string(), false));
903    }
904
905    #[test]
906    fn test_parse_selector_attributes_separate_attrs() {
907        let attrs: Vec<Attribute> = vec![
908            syn::parse_quote! { #[probar(entities = [Player])] },
909            syn::parse_quote! { #[probar(components = [Position])] },
910        ];
911        let (entities, components) = parse_selector_attributes(&attrs);
912        assert_eq!(entities, vec!["Player"]);
913        assert_eq!(components, vec!["Position"]);
914    }
915
916    #[test]
917    fn test_parse_timeout_attr_various_formats() {
918        // Different spacing
919        assert_eq!(parse_timeout_attr_from_str("timeout_ms=1000"), Some(1000));
920        assert_eq!(parse_timeout_attr_from_str("timeout_ms =2000"), Some(2000));
921        assert_eq!(parse_timeout_attr_from_str("timeout_ms= 3000"), Some(3000));
922    }
923
924    #[test]
925    fn test_parse_timeout_attr_with_other_attrs() {
926        let result = parse_timeout_attr_from_str("category = \"test\", timeout_ms = 7500");
927        assert_eq!(result, Some(7500));
928    }
929
930    #[test]
931    fn test_extract_list_items_malformed() {
932        // Missing closing bracket
933        let tokens = "probar(entities = [A, B";
934        let items = extract_list_items(tokens, 0);
935        assert!(items.is_empty());
936    }
937
938    #[test]
939    fn test_extract_list_items_reversed_brackets() {
940        // When ] comes before [, the slice would be invalid
941        // This tests with proper order but no content
942        let tokens = "probar(entities = [])";
943        let items = extract_list_items(tokens, 0);
944        assert!(items.is_empty());
945    }
946
947    #[test]
948    fn test_to_snake_case_all_uppercase() {
949        assert_eq!(to_snake_case("ABC"), "abc");
950        assert_eq!(to_snake_case("ABCDEF"), "abcdef");
951    }
952
953    #[test]
954    fn test_to_snake_case_single_char_words() {
955        assert_eq!(to_snake_case("AaBbCc"), "aa_bb_cc");
956    }
957
958    #[test]
959    fn test_generate_type_id_long_string() {
960        let long_name = "a".repeat(1000);
961        let id = generate_type_id(&long_name);
962        assert_ne!(id, 0);
963        // Verify it's deterministic
964        assert_eq!(id, generate_type_id(&long_name));
965    }
966
967    #[test]
968    fn test_extract_name_from_attr_list_style() {
969        // probar(skip) style - not name=value
970        let attr: Attribute = syn::parse_quote! {
971            #[probar(skip)]
972        };
973        let result = extract_name_from_attr(&attr);
974        assert_eq!(result, None);
975    }
976
977    #[test]
978    fn test_extract_fields_empty_named() {
979        let data = syn::parse_quote! {
980            struct Empty {}
981        };
982        let input: DeriveInput = data;
983        let fields = extract_fields(&input.data);
984        assert!(fields.is_empty());
985    }
986
987    #[test]
988    fn test_extract_fields_single_field() {
989        let data = syn::parse_quote! {
990            struct Single {
991                value: i32,
992            }
993        };
994        let input: DeriveInput = data;
995        let fields = extract_fields(&input.data);
996        assert_eq!(fields.len(), 1);
997        assert_eq!(fields[0].0, "value");
998    }
999
1000    #[test]
1001    fn test_extract_fields_tuple_single() {
1002        let data = syn::parse_quote! {
1003            struct Wrapper(String);
1004        };
1005        let input: DeriveInput = data;
1006        let fields = extract_fields(&input.data);
1007        assert_eq!(fields.len(), 1);
1008        assert_eq!(fields[0].0, "field_0");
1009    }
1010
1011    #[test]
1012    fn test_parse_selector_attributes_mixed_attrs() {
1013        // Non-probar attrs mixed in
1014        let attrs: Vec<Attribute> = vec![
1015            syn::parse_quote! { #[derive(Debug)] },
1016            syn::parse_quote! { #[probar(entities = [A, B])] },
1017            syn::parse_quote! { #[serde(rename_all = "camelCase")] },
1018            syn::parse_quote! { #[probar(components = [X, Y, Z])] },
1019        ];
1020        let (entities, components) = parse_selector_attributes(&attrs);
1021        assert_eq!(entities, vec!["A", "B"]);
1022        assert_eq!(components, vec!["X", "Y", "Z"]);
1023    }
1024
1025    #[test]
1026    fn test_extract_name_attribute_empty_list() {
1027        let attrs: Vec<Attribute> = vec![];
1028        let result = extract_name_attribute(&attrs);
1029        assert_eq!(result, None);
1030    }
1031
1032    #[test]
1033    fn test_generate_type_id_collision_resistance() {
1034        // Test that similar names produce different IDs
1035        let id_player = generate_type_id("player");
1036        let id_player1 = generate_type_id("player1");
1037        let id_players = generate_type_id("players");
1038        let id_player_caps = generate_type_id("Player");
1039
1040        assert_ne!(id_player, id_player1);
1041        assert_ne!(id_player, id_players);
1042        assert_ne!(id_player, id_player_caps); // Case sensitive
1043    }
1044}