Skip to main content

depyler_tooling/
test_generation.rs

1//! Automatic test generation for transpiled functions
2//!
3//! This module generates property-based tests using quickcheck
4//! for pure functions with appropriate properties.
5
6use depyler_hir::hir::{BinOp, HirExpr, HirFunction, HirStmt, Type};
7use anyhow::Result;
8use quote::quote;
9use syn;
10
11/// Configuration for test generation
12#[derive(Debug, Clone)]
13pub struct TestGenConfig {
14    /// Generate property-based tests
15    pub generate_property_tests: bool,
16    /// Generate example-based tests
17    pub generate_example_tests: bool,
18    /// Maximum number of test cases for quickcheck
19    pub max_test_cases: usize,
20    /// Generate shrinking tests
21    pub enable_shrinking: bool,
22}
23
24impl Default for TestGenConfig {
25    fn default() -> Self {
26        Self {
27            generate_property_tests: true,
28            generate_example_tests: true,
29            max_test_cases: 100,
30            enable_shrinking: true,
31        }
32    }
33}
34
35/// Test generator for HIR functions
36pub struct TestGenerator {
37    config: TestGenConfig,
38}
39
40impl TestGenerator {
41    pub fn new(config: TestGenConfig) -> Self {
42        Self { config }
43    }
44
45    /// Generate test items for a single function (without mod tests wrapper)
46    ///
47    /// DEPYLER-0280 FIX: This generates test functions only, not the module wrapper.
48    /// The module wrapper should be added once at the file level.
49    pub fn generate_test_items_for_function(
50        &self,
51        func: &HirFunction,
52    ) -> Result<Vec<proc_macro2::TokenStream>> {
53        // Only generate tests for pure functions
54        if !func.properties.is_pure {
55            return Ok(Vec::new());
56        }
57
58        let mut test_functions = Vec::new();
59
60        // Generate property-based tests
61        if self.config.generate_property_tests {
62            if let Some(prop_test) = self.generate_property_test(func)? {
63                test_functions.push(prop_test);
64            }
65        }
66
67        // Generate example-based tests
68        if self.config.generate_example_tests {
69            if let Some(example_test) = self.generate_example_test(func)? {
70                test_functions.push(example_test);
71            }
72        }
73
74        Ok(test_functions)
75    }
76
77    /// Generate a complete test module for multiple functions
78    ///
79    /// DEPYLER-0280 FIX: Wraps all test items in a single `mod tests {}` block.
80    /// This prevents "the name `tests` is defined multiple times" errors.
81    pub fn generate_tests_module(
82        &self,
83        functions: &[HirFunction],
84    ) -> Result<Option<proc_macro2::TokenStream>> {
85        let mut all_test_items = Vec::new();
86
87        // Collect test items from all functions
88        for func in functions {
89            let test_items = self.generate_test_items_for_function(func)?;
90            all_test_items.extend(test_items);
91        }
92
93        // If no tests were generated, return None
94        if all_test_items.is_empty() {
95            return Ok(None);
96        }
97
98        // Wrap all tests in a single mod tests block
99        Ok(Some(quote! {
100            #[cfg(test)]
101            mod tests {
102                use super::*;
103                use quickcheck::{quickcheck, TestResult};
104
105                #(#all_test_items)*
106            }
107        }))
108    }
109
110    /// Generate tests for a function if applicable (DEPRECATED - use generate_tests_module instead)
111    ///
112    /// DEPYLER-0280: This function is deprecated because it creates duplicate `mod tests {}` blocks.
113    /// Use `generate_tests_module()` for module-level test generation instead.
114    #[deprecated(
115        since = "3.19.22",
116        note = "Use generate_tests_module() to avoid duplicate mod tests blocks (DEPYLER-0280)"
117    )]
118    pub fn generate_tests(&self, func: &HirFunction) -> Result<Option<proc_macro2::TokenStream>> {
119        let test_items = self.generate_test_items_for_function(func)?;
120
121        if test_items.is_empty() {
122            return Ok(None);
123        }
124
125        Ok(Some(quote! {
126            #[cfg(test)]
127            mod tests {
128                use super::*;
129                use quickcheck::{quickcheck, TestResult};
130
131                #(#test_items)*
132            }
133        }))
134    }
135
136    /// Generate property-based test for a function
137    fn generate_property_test(
138        &self,
139        func: &HirFunction,
140    ) -> Result<Option<proc_macro2::TokenStream>> {
141        let func_name = syn::Ident::new(&func.name, proc_macro2::Span::call_site());
142        let test_name = syn::Ident::new(
143            &format!("quickcheck_{}", func.name),
144            proc_macro2::Span::call_site(),
145        );
146
147        // Determine properties to test based on function analysis
148        let properties = self.analyze_function_properties(func);
149
150        if properties.is_empty() {
151            return Ok(None);
152        }
153
154        // Generate parameter types and names for quickcheck
155        let param_types: Vec<_> = func
156            .params
157            .iter()
158            .map(|param| self.type_to_quickcheck_type(&param.ty))
159            .collect();
160
161        let param_names: Vec<_> = func
162            .params
163            .iter()
164            .map(|param| syn::Ident::new(&param.name, proc_macro2::Span::call_site()))
165            .collect();
166
167        // DEPYLER-0281: Pass function parameters for type-aware conversions
168        let property_checks: Vec<_> = properties
169            .iter()
170            .map(|prop| self.property_to_assertion(prop, &func_name, &param_names, &func.params))
171            .collect();
172
173        Ok(Some(quote! {
174            #[test]
175            fn #test_name() {
176                fn prop(#(#param_names: #param_types),*) -> TestResult {
177                    #(#property_checks)*
178                    TestResult::passed()
179                }
180
181                quickcheck(prop as fn(#(#param_types),*) -> TestResult);
182            }
183        }))
184    }
185
186    /// Generate example-based test
187    fn generate_example_test(
188        &self,
189        func: &HirFunction,
190    ) -> Result<Option<proc_macro2::TokenStream>> {
191        let test_name = syn::Ident::new(
192            &format!("test_{}_examples", func.name),
193            proc_macro2::Span::call_site(),
194        );
195
196        // Generate test cases based on function type
197        let test_cases = self.generate_test_cases(func);
198
199        if test_cases.is_empty() {
200            return Ok(None);
201        }
202
203        Ok(Some(quote! {
204            #[test]
205            fn #test_name() {
206                #(#test_cases)*
207            }
208        }))
209    }
210
211    /// Analyze function to determine testable properties
212    fn analyze_function_properties(&self, func: &HirFunction) -> Vec<TestProperty> {
213        // DEPYLER-0282 FIXED: String parameters now correctly use Cow<'_, str> instead of
214        // Cow<'static, str>, so property tests work properly with local String values.
215        // The DEPYLER-0281 workaround has been removed.
216
217        let mut properties = Vec::new();
218
219        // Check for common patterns
220        if self.is_identity_function(func) {
221            properties.push(TestProperty::Identity);
222        }
223
224        if self.is_commutative(func) {
225            properties.push(TestProperty::Commutative);
226        }
227
228        if self.is_associative(func) {
229            properties.push(TestProperty::Associative);
230        }
231
232        if self.returns_non_negative(func) {
233            properties.push(TestProperty::NonNegative);
234        }
235
236        if self.preserves_length(func) {
237            properties.push(TestProperty::LengthPreserving);
238        }
239
240        if self.is_idempotent(func) {
241            properties.push(TestProperty::Idempotent);
242        }
243
244        if self.is_sorting_function(func) {
245            properties.push(TestProperty::Sorted);
246            properties.push(TestProperty::SameElements);
247        }
248
249        properties
250    }
251
252    /// Check if function is an identity function
253    fn is_identity_function(&self, func: &HirFunction) -> bool {
254        // Simple case: function with one parameter that returns it unchanged
255        if func.params.len() == 1 && func.body.len() == 1 {
256            if let HirStmt::Return(Some(HirExpr::Var(name))) = &func.body[0] {
257                return name == &func.params[0].name;
258            }
259        }
260        false
261    }
262
263    /// Check if function is commutative (like addition)
264    fn is_commutative(&self, func: &HirFunction) -> bool {
265        if func.params.len() == 2 && func.body.len() == 1 {
266            if let HirStmt::Return(Some(HirExpr::Binary { op, left, right })) = &func.body[0] {
267                // DEPYLER-0286 FIX: String concatenation (BinOp::Add on strings) is NOT commutative!
268                // "ab" + "cd" ≠ "cd" + "ab"
269                // Only numeric addition is commutative, not string concatenation.
270
271                // Check if this is string concatenation (Add with String parameters)
272                let is_string_concat = matches!(op, BinOp::Add)
273                    && (matches!(func.params[0].ty, Type::String)
274                        || matches!(func.params[1].ty, Type::String));
275
276                // If it's string concatenation, it's NOT commutative
277                if is_string_concat {
278                    return false;
279                }
280
281                // Check if it's a commutative operation
282                matches!(
283                    op,
284                    BinOp::Add | BinOp::Mul | BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor
285                ) && self.is_simple_param_reference(left, &func.params[0].name)
286                    && self.is_simple_param_reference(right, &func.params[1].name)
287            } else {
288                false
289            }
290        } else {
291            false
292        }
293    }
294
295    /// Check if expression is a simple parameter reference
296    fn is_simple_param_reference(&self, expr: &HirExpr, param_name: &str) -> bool {
297        matches!(expr, HirExpr::Var(name) if name == param_name)
298    }
299
300    /// Check if function is associative
301    fn is_associative(&self, _func: &HirFunction) -> bool {
302        // This is more complex to detect automatically
303        // For now, return false
304        false
305    }
306
307    /// Check if function always returns non-negative values
308    fn returns_non_negative(&self, func: &HirFunction) -> bool {
309        // Check for abs-like patterns
310        func.name.contains("abs") || func.name.contains("magnitude")
311    }
312
313    /// Check if function preserves collection length
314    fn preserves_length(&self, func: &HirFunction) -> bool {
315        // Check if input and output are both lists/arrays
316        if func.params.len() == 1 {
317            if let (Type::List(_), Type::List(_)) = (&func.params[0].ty, &func.ret_type) {
318                // Simple heuristic: sorting and mapping functions preserve length
319                return func.name.contains("sort") || func.name.contains("map");
320            }
321        }
322        false
323    }
324
325    /// Check if function is idempotent
326    fn is_idempotent(&self, func: &HirFunction) -> bool {
327        func.name.contains("normalize") || func.name.contains("clean")
328    }
329
330    /// Check if function is a sorting function
331    fn is_sorting_function(&self, func: &HirFunction) -> bool {
332        // DEPYLER-0189: Must have at least one parameter to be a sorting function
333        !func.params.is_empty() && func.name.contains("sort")
334    }
335
336    /// Convert Python type to quickcheck-compatible type
337    #[allow(clippy::only_used_in_recursion)]
338    fn type_to_quickcheck_type(&self, ty: &Type) -> proc_macro2::TokenStream {
339        match ty {
340            Type::Int => quote! { i32 },
341            Type::Float => quote! { f64 },
342            Type::String => quote! { String },
343            Type::Bool => quote! { bool },
344            Type::List(inner) => {
345                let inner_type = self.type_to_quickcheck_type(inner);
346                quote! { Vec<#inner_type> }
347            }
348            _ => quote! { () }, // Unsupported type
349        }
350    }
351
352    /// Convert a property test argument to match function signature
353    ///
354    /// DEPYLER-0281 FIX: Property tests use simple QuickCheck types (String, Vec<T>),
355    /// but functions may have complex signatures (Cow<'static, str>, &Vec<T>).
356    /// This function generates conversion code to bridge the gap.
357    fn convert_arg_for_property_test(
358        &self,
359        ty: &Type,
360        arg_name: &syn::Ident,
361    ) -> proc_macro2::TokenStream {
362        match ty {
363            Type::String => {
364                // DEPYLER-0281 FIX: String parameters may become Cow<'static, str> or &str
365                // Use (&*arg).into() which converts via type inference:
366                //   - For `fn f(s: Cow<'static, str>)`: &str → Cow::Owned via From<String> after clone
367                //   - For `fn f(s: &str)`: &str → &str (into() is no-op when T: Copy)
368                // The &* dereferences String → str, then borrows as &str for inference.
369                quote! { (&*#arg_name).into() }
370            }
371            Type::List(_) => {
372                // List parameters become &Vec<T>
373                quote! { &#arg_name }
374            }
375            Type::Dict(_, _) => {
376                // Dict parameters become &HashMap<K, V>
377                quote! { &#arg_name }
378            }
379            _ => {
380                // Simple types (int, float, bool) - use directly with clone
381                quote! { #arg_name.clone() }
382            }
383        }
384    }
385
386    /// Convert property to assertion code
387    ///
388    /// DEPYLER-0281: Now accepts func_params to enable type-aware argument conversions.
389    /// This ensures property tests work with complex types like Cow<'static, str>.
390    fn property_to_assertion(
391        &self,
392        prop: &TestProperty,
393        func_name: &syn::Ident,
394        params: &[syn::Ident],
395        func_params: &[depyler_hir::hir::HirParam],
396    ) -> proc_macro2::TokenStream {
397        match prop {
398            TestProperty::Identity => {
399                // DEPYLER-0189: Bounds check before accessing params
400                if params.is_empty() || func_params.is_empty() {
401                    return quote! {};
402                }
403                let param = &params[0];
404
405                // DEPYLER-0281: Convert argument to match function signature
406                let param_converted = self.convert_arg_for_property_test(&func_params[0].ty, param);
407
408                quote! {
409                    let result = #func_name(#param_converted);
410                    if result != #param {
411                        return TestResult::failed();
412                    }
413                }
414            }
415            TestProperty::Commutative => {
416                // DEPYLER-0189: Bounds check before accessing params
417                if params.len() < 2 || func_params.len() < 2 {
418                    return quote! {};
419                }
420                let (a, b) = (&params[0], &params[1]);
421
422                // DEPYLER-0281 FIX: Convert arguments to match function signature
423                // QuickCheck generates simple types (String), but functions may expect
424                // complex types (Cow<'static, str>). Convert accordingly.
425                let a_converted = self.convert_arg_for_property_test(&func_params[0].ty, a);
426                let b_converted = self.convert_arg_for_property_test(&func_params[1].ty, b);
427
428                // DEPYLER-0284 FIX: Check for potential overflow with integer addition
429                // DEPYLER-0285 FIX: Check for NaN in float operations
430                let special_value_check = if matches!(func_params[0].ty, Type::Int)
431                    && matches!(func_params[1].ty, Type::Int)
432                {
433                    quote! {
434                        // Skip test if values would overflow
435                        if (#a > 0 && #b > i32::MAX - #a) || (#a < 0 && #b < i32::MIN - #a) {
436                            return TestResult::discard();
437                        }
438                    }
439                } else if matches!(func_params[0].ty, Type::Float)
440                    && matches!(func_params[1].ty, Type::Float)
441                {
442                    quote! {
443                        // Skip test if either value is NaN or infinite
444                        if #a.is_nan() || #b.is_nan() || #a.is_infinite() || #b.is_infinite() {
445                            return TestResult::discard();
446                        }
447                    }
448                } else {
449                    quote! {}
450                };
451
452                quote! {
453                    #special_value_check
454                    let result1 = #func_name(#a_converted, #b_converted);
455                    let result2 = #func_name(#b_converted, #a_converted);
456                    if result1 != result2 {
457                        return TestResult::failed();
458                    }
459                }
460            }
461            TestProperty::NonNegative => {
462                // DEPYLER-0281: Convert all arguments to match function signature
463                let converted_args: Vec<_> = params
464                    .iter()
465                    .zip(func_params.iter())
466                    .map(|(param, func_param)| {
467                        self.convert_arg_for_property_test(&func_param.ty, param)
468                    })
469                    .collect();
470
471                quote! {
472                    let result = #func_name(#(#converted_args),*);
473                    if result < 0 {
474                        return TestResult::failed();
475                    }
476                }
477            }
478            TestProperty::LengthPreserving => {
479                // DEPYLER-0189: Bounds check before accessing params
480                if params.is_empty() || func_params.is_empty() {
481                    return quote! {};
482                }
483                let param = &params[0];
484
485                // DEPYLER-0281: Convert argument to match function signature
486                let param_converted = self.convert_arg_for_property_test(&func_params[0].ty, param);
487
488                quote! {
489                    let input_len = #param.len();
490                    let result = #func_name(#param_converted);
491                    if result.len() != input_len {
492                        return TestResult::failed();
493                    }
494                }
495            }
496            TestProperty::Sorted => {
497                // DEPYLER-0281: Convert all arguments to match function signature
498                let converted_args: Vec<_> = params
499                    .iter()
500                    .zip(func_params.iter())
501                    .map(|(param, func_param)| {
502                        self.convert_arg_for_property_test(&func_param.ty, param)
503                    })
504                    .collect();
505
506                quote! {
507                    let result = #func_name(#(#converted_args),*);
508                    for i in 1..result.len() {
509                        if result[i-1] > result[i] {
510                            return TestResult::failed();
511                        }
512                    }
513                }
514            }
515            TestProperty::SameElements => {
516                // DEPYLER-0189: Bounds check before accessing params
517                if params.is_empty() || func_params.is_empty() {
518                    return quote! {};
519                }
520                let param = &params[0];
521
522                // DEPYLER-0281: Convert argument to match function signature
523                let param_converted = self.convert_arg_for_property_test(&func_params[0].ty, param);
524
525                quote! {
526                    let mut input_sorted = #param.clone();
527                    input_sorted.sort();
528                    let mut result = #func_name(#param_converted);
529                    result.sort();
530                    if input_sorted != result {
531                        return TestResult::failed();
532                    }
533                }
534            }
535            TestProperty::Idempotent => {
536                // DEPYLER-0281: Convert all arguments to match function signature
537                let converted_args: Vec<_> = params
538                    .iter()
539                    .zip(func_params.iter())
540                    .map(|(param, func_param)| {
541                        self.convert_arg_for_property_test(&func_param.ty, param)
542                    })
543                    .collect();
544
545                quote! {
546                    let once = #func_name(#(#converted_args),*);
547                    let twice = #func_name(once.clone());
548                    if once != twice {
549                        return TestResult::failed();
550                    }
551                }
552            }
553            _ => quote! {},
554        }
555    }
556
557    /// Generate example test cases
558    fn generate_test_cases(&self, func: &HirFunction) -> Vec<proc_macro2::TokenStream> {
559        let func_name = syn::Ident::new(&func.name, proc_macro2::Span::call_site());
560        let mut cases = Vec::new();
561
562        // Generate basic test cases based on function type and parameters
563        match (&func.ret_type, func.params.len()) {
564            (Type::Int, 0) => {
565                // No parameters - just call the function
566                cases.push(quote! {
567                    let _ = #func_name();
568                });
569            }
570            (Type::Int, 1) => {
571                // DEPYLER-0269: Check actual parameter type before generating test values
572                let param_type = &func.params[0].ty;
573                match param_type {
574                    Type::Int => {
575                        // Single integer parameter
576                        if func.name.contains("abs") {
577                            // Special case for absolute value functions
578                            cases.push(quote! {
579                                assert_eq!(#func_name(0), 0);
580                                assert_eq!(#func_name(1), 1);
581                                assert_eq!(#func_name(-1), 1);
582                                assert_eq!(#func_name(i32::MIN + 1), i32::MAX);
583                            });
584                        } else {
585                            // General case
586                            cases.push(quote! {
587                                assert_eq!(#func_name(0), 0);
588                                assert_eq!(#func_name(1), 1);
589                                assert_eq!(#func_name(-1), -1);
590                            });
591                        }
592                    }
593                    Type::List(_) => {
594                        // DEPYLER-0283 FIX: List parameter returning int
595                        // Detect if it's a sum function vs length function by name
596                        if func.name.contains("sum") {
597                            // Sum function - test sum of elements
598                            cases.push(quote! {
599                                assert_eq!(#func_name(&vec![]), 0);
600                                assert_eq!(#func_name(&vec![1]), 1);
601                                assert_eq!(#func_name(&vec![1, 2, 3]), 6);  // 1+2+3=6
602                            });
603                        } else if func.name.contains("len")
604                            || func.name.contains("count")
605                            || func.name.contains("size")
606                        {
607                            // Length/count function - test length
608                            cases.push(quote! {
609                                assert_eq!(#func_name(&vec![]), 0);
610                                assert_eq!(#func_name(&vec![1]), 1);
611                                assert_eq!(#func_name(&vec![1, 2, 3]), 3);
612                            });
613                        } else {
614                            // Unknown function - use conservative length-based test
615                            cases.push(quote! {
616                                assert_eq!(#func_name(&vec![]), 0);
617                                assert_eq!(#func_name(&vec![1]), 1);
618                                assert_eq!(#func_name(&vec![1, 2, 3]), 3);
619                            });
620                        }
621                    }
622                    Type::String => {
623                        // String parameter - generate string test cases
624                        cases.push(quote! {
625                            assert_eq!(#func_name(""), 0);
626                            assert_eq!(#func_name("a"), 1);
627                            assert_eq!(#func_name("abc"), 3);
628                        });
629                    }
630                    _ => {
631                        // Unsupported parameter type - skip test generation
632                    }
633                }
634            }
635            (Type::Int, 2)
636                if matches!(&func.params[0].ty, Type::Int)
637                    && matches!(&func.params[1].ty, Type::Int) =>
638            {
639                // Two integer parameters - test basic cases
640                cases.push(quote! {
641                    assert_eq!(#func_name(0, 0), 0);
642                    assert_eq!(#func_name(1, 2), 3);
643                    assert_eq!(#func_name(-1, 1), 0);
644                });
645            }
646            (Type::Bool, _) => {
647                // Test boolean functions
648                if func.params.len() == 1 {
649                    cases.push(quote! {
650                        // Test with edge cases
651                        let _ = #func_name(Default::default());
652                    });
653                }
654            }
655            (Type::List(_), _) => {
656                // Test with empty and single-element lists
657                if func.params.len() == 1 && matches!(&func.params[0].ty, Type::List(_)) {
658                    cases.push(quote! {
659                        assert_eq!(#func_name(vec![]), vec![]);
660                        assert_eq!(#func_name(vec![1]), vec![1]);
661                    });
662                }
663            }
664            _ => {}
665        }
666
667        cases
668    }
669}
670
671/// Properties that can be tested
672#[derive(Debug, Clone, PartialEq)]
673enum TestProperty {
674    Identity,
675    Commutative,
676    Associative,
677    NonNegative,
678    LengthPreserving,
679    Sorted,
680    SameElements,
681    Idempotent,
682    #[allow(dead_code)]
683    Distributive,
684    #[allow(dead_code)]
685    Monotonic,
686}
687
688#[cfg(test)]
689#[allow(clippy::field_reassign_with_default)]
690mod tests {
691    use super::*;
692    use depyler_hir::hir::FunctionProperties;
693    use depyler_annotations::TranspilationAnnotations;
694    use smallvec::smallvec;
695
696    fn make_pure_properties() -> FunctionProperties {
697        let mut props = FunctionProperties::default();
698        props.is_pure = true;
699        props
700    }
701
702    fn make_impure_properties() -> FunctionProperties {
703        let mut props = FunctionProperties::default();
704        props.is_pure = false;
705        props
706    }
707
708    fn make_param(name: &str, ty: Type) -> depyler_hir::hir::HirParam {
709        depyler_hir::hir::HirParam::new(name.to_string(), ty)
710    }
711
712    // TestGenConfig tests
713    #[test]
714    fn test_testgen_config_default() {
715        let config = TestGenConfig::default();
716        assert!(config.generate_property_tests);
717        assert!(config.generate_example_tests);
718        assert_eq!(config.max_test_cases, 100);
719        assert!(config.enable_shrinking);
720    }
721
722    #[test]
723    fn test_testgen_config_custom() {
724        let config = TestGenConfig {
725            generate_property_tests: false,
726            generate_example_tests: true,
727            max_test_cases: 50,
728            enable_shrinking: false,
729        };
730        assert!(!config.generate_property_tests);
731        assert!(config.generate_example_tests);
732        assert_eq!(config.max_test_cases, 50);
733        assert!(!config.enable_shrinking);
734    }
735
736    #[test]
737    fn test_testgen_config_clone() {
738        let config = TestGenConfig::default();
739        let cloned = config.clone();
740        assert_eq!(config.max_test_cases, cloned.max_test_cases);
741    }
742
743    // TestGenerator tests
744    #[test]
745    fn test_test_generator_new() {
746        let config = TestGenConfig::default();
747        let _gen = TestGenerator::new(config);
748    }
749
750    #[test]
751    fn test_generate_test_items_for_impure_function() {
752        let gen = TestGenerator::new(TestGenConfig::default());
753        let func = HirFunction {
754            name: "side_effect".to_string(),
755            params: smallvec![],
756            ret_type: Type::Int,
757            body: vec![],
758            properties: make_impure_properties(),
759            annotations: TranspilationAnnotations::default(),
760            docstring: None,
761        };
762        let result = gen.generate_test_items_for_function(&func).unwrap();
763        assert!(
764            result.is_empty(),
765            "Impure functions should not generate tests"
766        );
767    }
768
769    #[test]
770    fn test_generate_tests_module_empty() {
771        let gen = TestGenerator::new(TestGenConfig::default());
772        let result = gen.generate_tests_module(&[]).unwrap();
773        assert!(result.is_none(), "Empty function list should return None");
774    }
775
776    #[test]
777    fn test_generate_tests_module_impure_only() {
778        let gen = TestGenerator::new(TestGenConfig::default());
779        let func = HirFunction {
780            name: "impure".to_string(),
781            params: smallvec![],
782            ret_type: Type::Int,
783            body: vec![],
784            properties: make_impure_properties(),
785            annotations: TranspilationAnnotations::default(),
786            docstring: None,
787        };
788        let result = gen.generate_tests_module(&[func]).unwrap();
789        assert!(result.is_none(), "Only impure functions should return None");
790    }
791
792    // is_identity_function tests
793    #[test]
794    fn test_is_identity_function_true() {
795        let gen = TestGenerator::new(TestGenConfig::default());
796        let func = HirFunction {
797            name: "identity".to_string(),
798            params: smallvec![make_param("x", Type::Int)],
799            ret_type: Type::Int,
800            body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
801            properties: make_pure_properties(),
802            annotations: TranspilationAnnotations::default(),
803            docstring: None,
804        };
805        assert!(gen.is_identity_function(&func));
806    }
807
808    #[test]
809    fn test_is_identity_function_false_different_return() {
810        let gen = TestGenerator::new(TestGenConfig::default());
811        let func = HirFunction {
812            name: "not_identity".to_string(),
813            params: smallvec![make_param("x", Type::Int)],
814            ret_type: Type::Int,
815            body: vec![HirStmt::Return(Some(HirExpr::Var("y".to_string())))],
816            properties: make_pure_properties(),
817            annotations: TranspilationAnnotations::default(),
818            docstring: None,
819        };
820        assert!(!gen.is_identity_function(&func));
821    }
822
823    #[test]
824    fn test_is_identity_function_false_multiple_params() {
825        let gen = TestGenerator::new(TestGenConfig::default());
826        let func = HirFunction {
827            name: "two_params".to_string(),
828            params: smallvec![make_param("x", Type::Int), make_param("y", Type::Int)],
829            ret_type: Type::Int,
830            body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
831            properties: make_pure_properties(),
832            annotations: TranspilationAnnotations::default(),
833            docstring: None,
834        };
835        assert!(!gen.is_identity_function(&func));
836    }
837
838    #[test]
839    fn test_is_identity_function_false_multiple_stmts() {
840        let gen = TestGenerator::new(TestGenConfig::default());
841        let func = HirFunction {
842            name: "multiple_stmts".to_string(),
843            params: smallvec![make_param("x", Type::Int)],
844            ret_type: Type::Int,
845            body: vec![
846                HirStmt::Expr(HirExpr::Var("x".to_string())),
847                HirStmt::Return(Some(HirExpr::Var("x".to_string()))),
848            ],
849            properties: make_pure_properties(),
850            annotations: TranspilationAnnotations::default(),
851            docstring: None,
852        };
853        assert!(!gen.is_identity_function(&func));
854    }
855
856    // is_commutative tests
857    #[test]
858    fn test_is_commutative_add() {
859        let gen = TestGenerator::new(TestGenConfig::default());
860        let func = HirFunction {
861            name: "add".to_string(),
862            params: smallvec![make_param("a", Type::Int), make_param("b", Type::Int)],
863            ret_type: Type::Int,
864            body: vec![HirStmt::Return(Some(HirExpr::Binary {
865                op: BinOp::Add,
866                left: Box::new(HirExpr::Var("a".to_string())),
867                right: Box::new(HirExpr::Var("b".to_string())),
868            }))],
869            properties: make_pure_properties(),
870            annotations: TranspilationAnnotations::default(),
871            docstring: None,
872        };
873        assert!(gen.is_commutative(&func));
874    }
875
876    #[test]
877    fn test_is_commutative_mul() {
878        let gen = TestGenerator::new(TestGenConfig::default());
879        let func = HirFunction {
880            name: "mul".to_string(),
881            params: smallvec![make_param("a", Type::Int), make_param("b", Type::Int)],
882            ret_type: Type::Int,
883            body: vec![HirStmt::Return(Some(HirExpr::Binary {
884                op: BinOp::Mul,
885                left: Box::new(HirExpr::Var("a".to_string())),
886                right: Box::new(HirExpr::Var("b".to_string())),
887            }))],
888            properties: make_pure_properties(),
889            annotations: TranspilationAnnotations::default(),
890            docstring: None,
891        };
892        assert!(gen.is_commutative(&func));
893    }
894
895    #[test]
896    fn test_is_commutative_sub_false() {
897        let gen = TestGenerator::new(TestGenConfig::default());
898        let func = HirFunction {
899            name: "sub".to_string(),
900            params: smallvec![make_param("a", Type::Int), make_param("b", Type::Int)],
901            ret_type: Type::Int,
902            body: vec![HirStmt::Return(Some(HirExpr::Binary {
903                op: BinOp::Sub,
904                left: Box::new(HirExpr::Var("a".to_string())),
905                right: Box::new(HirExpr::Var("b".to_string())),
906            }))],
907            properties: make_pure_properties(),
908            annotations: TranspilationAnnotations::default(),
909            docstring: None,
910        };
911        assert!(!gen.is_commutative(&func));
912    }
913
914    #[test]
915    fn test_is_commutative_string_concat_false() {
916        // DEPYLER-0286: String concatenation is NOT commutative
917        let gen = TestGenerator::new(TestGenConfig::default());
918        let func = HirFunction {
919            name: "concat".to_string(),
920            params: smallvec![make_param("a", Type::String), make_param("b", Type::String)],
921            ret_type: Type::String,
922            body: vec![HirStmt::Return(Some(HirExpr::Binary {
923                op: BinOp::Add,
924                left: Box::new(HirExpr::Var("a".to_string())),
925                right: Box::new(HirExpr::Var("b".to_string())),
926            }))],
927            properties: make_pure_properties(),
928            annotations: TranspilationAnnotations::default(),
929            docstring: None,
930        };
931        assert!(!gen.is_commutative(&func));
932    }
933
934    #[test]
935    fn test_is_commutative_single_param_false() {
936        let gen = TestGenerator::new(TestGenConfig::default());
937        let func = HirFunction {
938            name: "single".to_string(),
939            params: smallvec![make_param("a", Type::Int)],
940            ret_type: Type::Int,
941            body: vec![HirStmt::Return(Some(HirExpr::Var("a".to_string())))],
942            properties: make_pure_properties(),
943            annotations: TranspilationAnnotations::default(),
944            docstring: None,
945        };
946        assert!(!gen.is_commutative(&func));
947    }
948
949    // is_simple_param_reference tests
950    #[test]
951    fn test_is_simple_param_reference_true() {
952        let gen = TestGenerator::new(TestGenConfig::default());
953        let expr = HirExpr::Var("x".to_string());
954        assert!(gen.is_simple_param_reference(&expr, "x"));
955    }
956
957    #[test]
958    fn test_is_simple_param_reference_false_different_name() {
959        let gen = TestGenerator::new(TestGenConfig::default());
960        let expr = HirExpr::Var("x".to_string());
961        assert!(!gen.is_simple_param_reference(&expr, "y"));
962    }
963
964    #[test]
965    fn test_is_simple_param_reference_false_not_var() {
966        let gen = TestGenerator::new(TestGenConfig::default());
967        let expr = HirExpr::Literal(depyler_hir::hir::Literal::Int(1));
968        assert!(!gen.is_simple_param_reference(&expr, "x"));
969    }
970
971    // is_associative tests
972    #[test]
973    fn test_is_associative_always_false() {
974        let gen = TestGenerator::new(TestGenConfig::default());
975        let func = HirFunction {
976            name: "add".to_string(),
977            params: smallvec![],
978            ret_type: Type::Int,
979            body: vec![],
980            properties: make_pure_properties(),
981            annotations: TranspilationAnnotations::default(),
982            docstring: None,
983        };
984        assert!(!gen.is_associative(&func));
985    }
986
987    // returns_non_negative tests
988    #[test]
989    fn test_returns_non_negative_abs() {
990        let gen = TestGenerator::new(TestGenConfig::default());
991        let func = HirFunction {
992            name: "my_abs".to_string(),
993            params: smallvec![],
994            ret_type: Type::Int,
995            body: vec![],
996            properties: make_pure_properties(),
997            annotations: TranspilationAnnotations::default(),
998            docstring: None,
999        };
1000        assert!(gen.returns_non_negative(&func));
1001    }
1002
1003    #[test]
1004    fn test_returns_non_negative_magnitude() {
1005        let gen = TestGenerator::new(TestGenConfig::default());
1006        let func = HirFunction {
1007            name: "magnitude".to_string(),
1008            params: smallvec![],
1009            ret_type: Type::Int,
1010            body: vec![],
1011            properties: make_pure_properties(),
1012            annotations: TranspilationAnnotations::default(),
1013            docstring: None,
1014        };
1015        assert!(gen.returns_non_negative(&func));
1016    }
1017
1018    #[test]
1019    fn test_returns_non_negative_false() {
1020        let gen = TestGenerator::new(TestGenConfig::default());
1021        let func = HirFunction {
1022            name: "subtract".to_string(),
1023            params: smallvec![],
1024            ret_type: Type::Int,
1025            body: vec![],
1026            properties: make_pure_properties(),
1027            annotations: TranspilationAnnotations::default(),
1028            docstring: None,
1029        };
1030        assert!(!gen.returns_non_negative(&func));
1031    }
1032
1033    // preserves_length tests
1034    #[test]
1035    fn test_preserves_length_sort() {
1036        let gen = TestGenerator::new(TestGenConfig::default());
1037        let func = HirFunction {
1038            name: "my_sort".to_string(),
1039            params: smallvec![make_param("arr", Type::List(Box::new(Type::Int)))],
1040            ret_type: Type::List(Box::new(Type::Int)),
1041            body: vec![],
1042            properties: make_pure_properties(),
1043            annotations: TranspilationAnnotations::default(),
1044            docstring: None,
1045        };
1046        assert!(gen.preserves_length(&func));
1047    }
1048
1049    #[test]
1050    fn test_preserves_length_map() {
1051        let gen = TestGenerator::new(TestGenConfig::default());
1052        let func = HirFunction {
1053            name: "double_map".to_string(),
1054            params: smallvec![make_param("arr", Type::List(Box::new(Type::Int)))],
1055            ret_type: Type::List(Box::new(Type::Int)),
1056            body: vec![],
1057            properties: make_pure_properties(),
1058            annotations: TranspilationAnnotations::default(),
1059            docstring: None,
1060        };
1061        assert!(gen.preserves_length(&func));
1062    }
1063
1064    #[test]
1065    fn test_preserves_length_false_no_list() {
1066        let gen = TestGenerator::new(TestGenConfig::default());
1067        let func = HirFunction {
1068            name: "my_sort".to_string(),
1069            params: smallvec![make_param("x", Type::Int)],
1070            ret_type: Type::Int,
1071            body: vec![],
1072            properties: make_pure_properties(),
1073            annotations: TranspilationAnnotations::default(),
1074            docstring: None,
1075        };
1076        assert!(!gen.preserves_length(&func));
1077    }
1078
1079    // is_idempotent tests
1080    #[test]
1081    fn test_is_idempotent_normalize() {
1082        let gen = TestGenerator::new(TestGenConfig::default());
1083        let func = HirFunction {
1084            name: "normalize_path".to_string(),
1085            params: smallvec![],
1086            ret_type: Type::String,
1087            body: vec![],
1088            properties: make_pure_properties(),
1089            annotations: TranspilationAnnotations::default(),
1090            docstring: None,
1091        };
1092        assert!(gen.is_idempotent(&func));
1093    }
1094
1095    #[test]
1096    fn test_is_idempotent_clean() {
1097        let gen = TestGenerator::new(TestGenConfig::default());
1098        let func = HirFunction {
1099            name: "clean_text".to_string(),
1100            params: smallvec![],
1101            ret_type: Type::String,
1102            body: vec![],
1103            properties: make_pure_properties(),
1104            annotations: TranspilationAnnotations::default(),
1105            docstring: None,
1106        };
1107        assert!(gen.is_idempotent(&func));
1108    }
1109
1110    #[test]
1111    fn test_is_idempotent_false() {
1112        let gen = TestGenerator::new(TestGenConfig::default());
1113        let func = HirFunction {
1114            name: "increment".to_string(),
1115            params: smallvec![],
1116            ret_type: Type::Int,
1117            body: vec![],
1118            properties: make_pure_properties(),
1119            annotations: TranspilationAnnotations::default(),
1120            docstring: None,
1121        };
1122        assert!(!gen.is_idempotent(&func));
1123    }
1124
1125    // is_sorting_function tests
1126    #[test]
1127    fn test_is_sorting_function_true() {
1128        let gen = TestGenerator::new(TestGenConfig::default());
1129        let func = HirFunction {
1130            name: "bubble_sort".to_string(),
1131            params: smallvec![make_param("arr", Type::List(Box::new(Type::Int)))],
1132            ret_type: Type::List(Box::new(Type::Int)),
1133            body: vec![],
1134            properties: make_pure_properties(),
1135            annotations: TranspilationAnnotations::default(),
1136            docstring: None,
1137        };
1138        assert!(gen.is_sorting_function(&func));
1139    }
1140
1141    #[test]
1142    fn test_is_sorting_function_no_params_false() {
1143        // DEPYLER-0189: Sorting function must have at least one param
1144        let gen = TestGenerator::new(TestGenConfig::default());
1145        let func = HirFunction {
1146            name: "get_sorted".to_string(),
1147            params: smallvec![],
1148            ret_type: Type::List(Box::new(Type::Int)),
1149            body: vec![],
1150            properties: make_pure_properties(),
1151            annotations: TranspilationAnnotations::default(),
1152            docstring: None,
1153        };
1154        assert!(!gen.is_sorting_function(&func));
1155    }
1156
1157    #[test]
1158    fn test_is_sorting_function_no_sort_in_name() {
1159        let gen = TestGenerator::new(TestGenConfig::default());
1160        let func = HirFunction {
1161            name: "order_items".to_string(),
1162            params: smallvec![make_param("arr", Type::List(Box::new(Type::Int)))],
1163            ret_type: Type::List(Box::new(Type::Int)),
1164            body: vec![],
1165            properties: make_pure_properties(),
1166            annotations: TranspilationAnnotations::default(),
1167            docstring: None,
1168        };
1169        assert!(!gen.is_sorting_function(&func));
1170    }
1171
1172    // type_to_quickcheck_type tests
1173    #[test]
1174    fn test_type_to_quickcheck_type_int() {
1175        let gen = TestGenerator::new(TestGenConfig::default());
1176        let result = gen.type_to_quickcheck_type(&Type::Int);
1177        assert_eq!(result.to_string(), "i32");
1178    }
1179
1180    #[test]
1181    fn test_type_to_quickcheck_type_float() {
1182        let gen = TestGenerator::new(TestGenConfig::default());
1183        let result = gen.type_to_quickcheck_type(&Type::Float);
1184        assert_eq!(result.to_string(), "f64");
1185    }
1186
1187    #[test]
1188    fn test_type_to_quickcheck_type_string() {
1189        let gen = TestGenerator::new(TestGenConfig::default());
1190        let result = gen.type_to_quickcheck_type(&Type::String);
1191        assert_eq!(result.to_string(), "String");
1192    }
1193
1194    #[test]
1195    fn test_type_to_quickcheck_type_bool() {
1196        let gen = TestGenerator::new(TestGenConfig::default());
1197        let result = gen.type_to_quickcheck_type(&Type::Bool);
1198        assert_eq!(result.to_string(), "bool");
1199    }
1200
1201    #[test]
1202    fn test_type_to_quickcheck_type_list() {
1203        let gen = TestGenerator::new(TestGenConfig::default());
1204        let result = gen.type_to_quickcheck_type(&Type::List(Box::new(Type::Int)));
1205        assert_eq!(result.to_string(), "Vec < i32 >");
1206    }
1207
1208    #[test]
1209    fn test_type_to_quickcheck_type_unsupported() {
1210        let gen = TestGenerator::new(TestGenConfig::default());
1211        let result = gen.type_to_quickcheck_type(&Type::None);
1212        assert_eq!(result.to_string(), "()");
1213    }
1214
1215    // analyze_function_properties tests
1216    #[test]
1217    fn test_analyze_function_properties_identity() {
1218        let gen = TestGenerator::new(TestGenConfig::default());
1219        let func = HirFunction {
1220            name: "identity".to_string(),
1221            params: smallvec![make_param("x", Type::Int)],
1222            ret_type: Type::Int,
1223            body: vec![HirStmt::Return(Some(HirExpr::Var("x".to_string())))],
1224            properties: make_pure_properties(),
1225            annotations: TranspilationAnnotations::default(),
1226            docstring: None,
1227        };
1228        let props = gen.analyze_function_properties(&func);
1229        assert!(props.contains(&TestProperty::Identity));
1230    }
1231
1232    #[test]
1233    fn test_analyze_function_properties_commutative() {
1234        let gen = TestGenerator::new(TestGenConfig::default());
1235        let func = HirFunction {
1236            name: "add".to_string(),
1237            params: smallvec![make_param("a", Type::Int), make_param("b", Type::Int)],
1238            ret_type: Type::Int,
1239            body: vec![HirStmt::Return(Some(HirExpr::Binary {
1240                op: BinOp::Add,
1241                left: Box::new(HirExpr::Var("a".to_string())),
1242                right: Box::new(HirExpr::Var("b".to_string())),
1243            }))],
1244            properties: make_pure_properties(),
1245            annotations: TranspilationAnnotations::default(),
1246            docstring: None,
1247        };
1248        let props = gen.analyze_function_properties(&func);
1249        assert!(props.contains(&TestProperty::Commutative));
1250    }
1251
1252    #[test]
1253    fn test_analyze_function_properties_sorting() {
1254        let gen = TestGenerator::new(TestGenConfig::default());
1255        let func = HirFunction {
1256            name: "my_sort".to_string(),
1257            params: smallvec![make_param("arr", Type::List(Box::new(Type::Int)))],
1258            ret_type: Type::List(Box::new(Type::Int)),
1259            body: vec![],
1260            properties: make_pure_properties(),
1261            annotations: TranspilationAnnotations::default(),
1262            docstring: None,
1263        };
1264        let props = gen.analyze_function_properties(&func);
1265        assert!(props.contains(&TestProperty::Sorted));
1266        assert!(props.contains(&TestProperty::SameElements));
1267        assert!(props.contains(&TestProperty::LengthPreserving));
1268    }
1269
1270    // TestProperty tests
1271    #[test]
1272    fn test_property_eq() {
1273        assert_eq!(TestProperty::Identity, TestProperty::Identity);
1274        assert_ne!(TestProperty::Identity, TestProperty::Commutative);
1275    }
1276
1277    #[test]
1278    fn test_property_clone() {
1279        let prop = TestProperty::NonNegative;
1280        let cloned = prop.clone();
1281        assert_eq!(prop, cloned);
1282    }
1283
1284    #[test]
1285    fn test_property_debug() {
1286        let prop = TestProperty::Idempotent;
1287        let debug_str = format!("{:?}", prop);
1288        assert_eq!(debug_str, "Idempotent");
1289    }
1290}