Skip to main content

scirs2_ndimage/
api_compatibility_verification.rs

1//! API Compatibility Verification with SciPy ndimage
2//!
3//! This module provides comprehensive testing utilities to verify that
4//! scirs2-ndimage maintains API compatibility with SciPy's ndimage module.
5//! It includes parameter validation, behavior verification, and migration
6//! guidance for any incompatibilities.
7
8type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
9use crate::filters::*;
10use crate::interpolation::*;
11use crate::measurements::*;
12use crate::morphology::*;
13use crate::scipy_compat_layer;
14use scirs2_core::ndarray::Array2;
15
16/// API compatibility test result
17#[derive(Debug, Clone)]
18pub struct ApiCompatibilityResult {
19    /// Function name being tested
20    pub function_name: String,
21    /// Specific test case description
22    pub test_case: String,
23    /// Whether the API is compatible
24    pub compatible: bool,
25    /// Compatibility score (0.0 = incompatible, 1.0 = fully compatible)
26    pub compatibility_score: f64,
27    /// List of incompatible parameters
28    pub incompatible_parameters: Vec<String>,
29    /// Error messages for failures
30    pub error_messages: Vec<String>,
31    /// Suggested workarounds or fixes
32    pub suggestions: Vec<String>,
33    /// SciPy reference behavior description
34    pub scipy_behavior: String,
35    /// scirs2-ndimage behavior description
36    pub scirs2_behavior: String,
37}
38
39/// Parameter compatibility test
40#[derive(Debug, Clone)]
41pub struct ParameterTest {
42    /// Parameter name
43    pub name: String,
44    /// Test description
45    pub description: String,
46    /// Test function that returns (success, error_message)
47    pub test_fn: fn() -> (bool, Option<String>),
48    /// Expected behavior in SciPy
49    pub scipy_expected: String,
50    /// Priority level (High, Medium, Low)
51    pub priority: String,
52}
53
54/// Comprehensive API compatibility tester
55pub struct ApiCompatibilityTester {
56    /// Test results
57    results: Vec<ApiCompatibilityResult>,
58    /// Overall compatibility score
59    overall_score: f64,
60    /// Configuration for testing
61    config: CompatibilityConfig,
62}
63
64/// Configuration for compatibility testing
65#[derive(Debug, Clone)]
66pub struct CompatibilityConfig {
67    /// Whether to test edge cases
68    pub test_edge_cases: bool,
69    /// Whether to test error conditions
70    pub test_error_conditions: bool,
71    /// Whether to test performance characteristics
72    pub test_performance: bool,
73    /// Tolerance for numerical comparisons
74    pub numerical_tolerance: f64,
75    /// Maximum array size for testing
76    pub max_test_size: usize,
77}
78
79impl Default for CompatibilityConfig {
80    fn default() -> Self {
81        Self {
82            test_edge_cases: true,
83            test_error_conditions: true,
84            test_performance: false, // Expensive, disabled by default
85            numerical_tolerance: 1e-10,
86            max_test_size: 1000,
87        }
88    }
89}
90
91impl ApiCompatibilityTester {
92    /// Create a new compatibility tester
93    pub fn new() -> Self {
94        Self::with_config(CompatibilityConfig::default())
95    }
96
97    /// Create a new compatibility tester with custom configuration
98    pub fn with_config(config: CompatibilityConfig) -> Self {
99        Self {
100            results: Vec::new(),
101            overall_score: 0.0,
102            config,
103        }
104    }
105
106    /// Test all filter function APIs for compatibility
107    pub fn test_filter_apis(&mut self) -> Result<()> {
108        // Test gaussian_filter API compatibility
109        self.test_gaussian_filter_api()?;
110
111        // Test median_filter API compatibility
112        self.test_median_filter_api()?;
113
114        // Test uniform_filter API compatibility
115        self.test_uniform_filter_api()?;
116
117        // Test sobel filter API compatibility
118        self.test_sobel_filter_api()?;
119
120        // Test rank filters API compatibility
121        self.test_rank_filter_apis()?;
122
123        Ok(())
124    }
125
126    fn test_gaussian_filter_api(&mut self) -> Result<()> {
127        let mut incompatible_params = Vec::new();
128        let mut error_messages = Vec::new();
129        let mut suggestions = Vec::new();
130
131        // Test 1: Basic parameter compatibility
132        let input: Array2<f64> = Array2::zeros((10, 10));
133
134        // Test sigma parameter - should accept scalar and array
135        let test1_success = gaussian_filter(&input, 1.0, None, None).is_ok();
136        if !test1_success {
137            incompatible_params.push("sigma_scalar".to_string());
138            error_messages.push("Scalar sigma parameter not supported".to_string());
139        }
140
141        // Test output parameter - scirs2 returns result instead of in-place
142        let scipy_behavior = "SciPy accepts optional output array for in-place operations";
143        let scirs2_behavior = "scirs2-ndimage always returns new array, no in-place operations";
144        suggestions.push("Use returned array instead of in-place modification".to_string());
145
146        // Test mode parameter compatibility
147        let mode_test = gaussian_filter(&input, 1.0, Some(BorderMode::Constant), None).is_ok();
148        if !mode_test {
149            incompatible_params.push("mode".to_string());
150            error_messages.push("BorderMode enum may not match SciPy string modes".to_string());
151            suggestions.push("Use BorderMode enum instead of strings".to_string());
152        }
153
154        // Test cval parameter
155        let cval_test = gaussian_filter(&input, 1.0, Some(BorderMode::Constant), Some(0.0)).is_ok();
156        if !cval_test {
157            incompatible_params.push("cval".to_string());
158            error_messages.push("Constant value parameter handling differs".to_string());
159        }
160
161        // Edge case: Very small sigma
162        let small_sigma_test = gaussian_filter(&input, 1e-10, None, None).is_ok();
163        if !small_sigma_test && self.config.test_edge_cases {
164            incompatible_params.push("sigma_edge_case".to_string());
165            error_messages.push("Very small sigma values may be handled differently".to_string());
166        }
167
168        // Error condition: Negative sigma
169        let negative_sigma_test = gaussian_filter(&input, -1.0, None, None).is_err();
170        if !negative_sigma_test && self.config.test_error_conditions {
171            incompatible_params.push("sigma_validation".to_string());
172            error_messages.push("Negative sigma should raise error".to_string());
173        }
174
175        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 6.0); // 6 total tests
176
177        self.results.push(ApiCompatibilityResult {
178            function_name: "gaussian_filter".to_string(),
179            test_case: "Parameter compatibility and behavior".to_string(),
180            compatible: incompatible_params.is_empty(),
181            compatibility_score,
182            incompatible_parameters: incompatible_params,
183            error_messages,
184            suggestions,
185            scipy_behavior: scipy_behavior.to_string(),
186            scirs2_behavior: scirs2_behavior.to_string(),
187        });
188
189        Ok(())
190    }
191
192    fn test_median_filter_api(&mut self) -> Result<()> {
193        let mut incompatible_params = Vec::new();
194        let mut error_messages = Vec::new();
195        let mut suggestions = Vec::new();
196
197        let input: Array2<f64> = Array2::zeros((10, 10));
198
199        // Test size parameter - should accept various formats
200        let size_array_test = median_filter(&input, &[3, 3], None).is_ok();
201        if !size_array_test {
202            incompatible_params.push("size_array".to_string());
203            error_messages.push("Array size parameter not supported".to_string());
204        }
205
206        // Test footprint parameter - SciPy has footprint, we have size
207        suggestions.push("Use size parameter instead of footprint for filter kernel".to_string());
208
209        // Test mode parameter
210        let mode_test = median_filter(&input, &[3, 3], Some(BorderMode::Reflect)).is_ok();
211        if !mode_test {
212            incompatible_params.push("mode".to_string());
213            error_messages.push("Border mode handling may differ".to_string());
214        }
215
216        // Edge case: Size = 1 (no filtering)
217        let size_one_test = median_filter(&input, &[1, 1], None).is_ok();
218        if !size_one_test && self.config.test_edge_cases {
219            incompatible_params.push("size_edge_case".to_string());
220            error_messages.push("Size=1 edge case handling differs".to_string());
221        }
222
223        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 4.0);
224
225        self.results.push(ApiCompatibilityResult {
226            function_name: "median_filter".to_string(),
227            test_case: "Parameter and edge case compatibility".to_string(),
228            compatible: incompatible_params.is_empty(),
229            compatibility_score,
230            incompatible_parameters: incompatible_params,
231            error_messages,
232            suggestions,
233            scipy_behavior: "Accepts footprint or size, various modes".to_string(),
234            scirs2_behavior: "Accepts size array and BorderMode enum".to_string(),
235        });
236
237        Ok(())
238    }
239
240    fn test_uniform_filter_api(&mut self) -> Result<()> {
241        let mut incompatible_params = Vec::new();
242        let mut error_messages = Vec::new();
243
244        let input: Array2<f64> = Array2::zeros((10, 10));
245
246        // Test basic functionality
247        let basic_test = uniform_filter(&input, &[3, 3], None, None).is_ok();
248        if !basic_test {
249            incompatible_params.push("basic_functionality".to_string());
250            error_messages.push("Basic uniform filter functionality differs".to_string());
251        }
252
253        // Test mode parameter
254        let mode_test = uniform_filter(&input, &[3, 3], Some(BorderMode::Wrap), None).is_ok();
255        if !mode_test {
256            incompatible_params.push("mode".to_string());
257            error_messages.push("Wrap mode may not be supported".to_string());
258        }
259
260        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 2.0);
261
262        self.results.push(ApiCompatibilityResult {
263            function_name: "uniform_filter".to_string(),
264            test_case: "Basic functionality".to_string(),
265            compatible: incompatible_params.is_empty(),
266            compatibility_score,
267            incompatible_parameters: incompatible_params,
268            error_messages,
269            suggestions: vec!["Ensure all border modes are supported".to_string()],
270            scipy_behavior: "Supports all scipy.ndimage border modes".to_string(),
271            scirs2_behavior: "Supports BorderMode enum variants".to_string(),
272        });
273
274        Ok(())
275    }
276
277    fn test_sobel_filter_api(&mut self) -> Result<()> {
278        let mut incompatible_params = Vec::new();
279        let mut error_messages = Vec::new();
280
281        let input: Array2<f64> = Array2::zeros((10, 10));
282
283        // Test axis parameter
284        let axis_test = sobel(&input, 0, None).is_ok();
285        if !axis_test {
286            incompatible_params.push("axis".to_string());
287            error_messages.push("Axis parameter handling differs".to_string());
288        }
289
290        // Test without axis (should compute magnitude)
291        let no_axis_test = sobel(&input, 1, None).is_ok();
292        if !no_axis_test {
293            incompatible_params.push("axis_none".to_string());
294            error_messages.push("Default behavior without axis differs".to_string());
295        }
296
297        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 2.0);
298
299        self.results.push(ApiCompatibilityResult {
300            function_name: "sobel".to_string(),
301            test_case: "Axis parameter handling".to_string(),
302            compatible: incompatible_params.is_empty(),
303            compatibility_score,
304            incompatible_parameters: incompatible_params,
305            error_messages,
306            suggestions: vec!["Verify axis parameter behavior matches SciPy".to_string()],
307            scipy_behavior: "axis=None computes gradient magnitude".to_string(),
308            scirs2_behavior: "Axis parameter controls gradient direction".to_string(),
309        });
310
311        Ok(())
312    }
313
314    fn test_rank_filter_apis(&mut self) -> Result<()> {
315        let mut incompatible_params = Vec::new();
316        let mut error_messages = Vec::new();
317
318        let input: Array2<f64> = Array2::zeros((10, 10));
319
320        // Test minimum_filter
321        let min_test = minimum_filter(&input, &[3, 3], None, None).is_ok();
322        if !min_test {
323            incompatible_params.push("minimum_filter".to_string());
324            error_messages.push("minimum_filter API differs".to_string());
325        }
326
327        // Test maximum_filter
328        let max_test = maximum_filter(&input, &[3, 3], None, None).is_ok();
329        if !max_test {
330            incompatible_params.push("maximum_filter".to_string());
331            error_messages.push("maximum_filter API differs".to_string());
332        }
333
334        // Test percentile_filter
335        let percentile_test = percentile_filter(&input, 50.0, &[3, 3], None).is_ok();
336        if !percentile_test {
337            incompatible_params.push("percentile_filter".to_string());
338            error_messages.push("percentile_filter API differs".to_string());
339        }
340
341        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 3.0);
342
343        self.results.push(ApiCompatibilityResult {
344            function_name: "rank_filters".to_string(),
345            test_case: "Rank-based filters compatibility".to_string(),
346            compatible: incompatible_params.is_empty(),
347            compatibility_score,
348            incompatible_parameters: incompatible_params,
349            error_messages,
350            suggestions: vec!["Ensure rank filters match SciPy parameter order".to_string()],
351            scipy_behavior: "Standard rank filter implementations".to_string(),
352            scirs2_behavior: "Rank filters with size and mode parameters".to_string(),
353        });
354
355        Ok(())
356    }
357
358    /// Test morphological operation APIs
359    pub fn test_morphology_apis(&mut self) -> Result<()> {
360        self.test_binary_morphology_api()?;
361        self.test_grayscale_morphology_api()?;
362        Ok(())
363    }
364
365    fn test_binary_morphology_api(&mut self) -> Result<()> {
366        let mut incompatible_params = Vec::new();
367        let mut error_messages = Vec::new();
368
369        let input = Array2::from_elem((10, 10), true);
370
371        // Test binary_erosion with default structure
372        let erosion_test = binary_erosion(&input, None, None, None, None, None, None).is_ok();
373        if !erosion_test {
374            incompatible_params.push("binary_erosion".to_string());
375            error_messages.push("binary_erosion default parameters differ".to_string());
376        }
377
378        // Test binary_dilation
379        let dilation_test = binary_dilation(&input, None, None, None, None, None, None).is_ok();
380        if !dilation_test {
381            incompatible_params.push("binary_dilation".to_string());
382            error_messages.push("binary_dilation default parameters differ".to_string());
383        }
384
385        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 2.0);
386
387        self.results.push(ApiCompatibilityResult {
388            function_name: "binary_morphology".to_string(),
389            test_case: "Binary morphological operations".to_string(),
390            compatible: incompatible_params.is_empty(),
391            compatibility_score,
392            incompatible_parameters: incompatible_params,
393            error_messages,
394            suggestions: vec!["Verify structuring element defaults".to_string()],
395            scipy_behavior: "Uses cross-shaped structuring element by default".to_string(),
396            scirs2_behavior: "Default structuring element may differ".to_string(),
397        });
398
399        Ok(())
400    }
401
402    fn test_grayscale_morphology_api(&mut self) -> Result<()> {
403        let mut incompatible_params = Vec::new();
404        let mut error_messages = Vec::new();
405
406        let input: Array2<f64> = Array2::zeros((10, 10));
407
408        // Test grey_erosion
409        let erosion_test = grey_erosion(&input, None, None, None, None, None).is_ok();
410        if !erosion_test {
411            incompatible_params.push("grey_erosion".to_string());
412            error_messages.push("grey_erosion API differs".to_string());
413        }
414
415        // Test grey_dilation
416        let dilation_test = grey_dilation(&input, None, None, None, None, None).is_ok();
417        if !dilation_test {
418            incompatible_params.push("grey_dilation".to_string());
419            error_messages.push("grey_dilation API differs".to_string());
420        }
421
422        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 2.0);
423
424        self.results.push(ApiCompatibilityResult {
425            function_name: "grayscale_morphology".to_string(),
426            test_case: "Grayscale morphological operations".to_string(),
427            compatible: incompatible_params.is_empty(),
428            compatibility_score,
429            incompatible_parameters: incompatible_params,
430            error_messages,
431            suggestions: vec!["Ensure grayscale morphology parameters match SciPy".to_string()],
432            scipy_behavior: "Standard grayscale morphology with size/footprint".to_string(),
433            scirs2_behavior: "Grayscale morphology with structure parameters".to_string(),
434        });
435
436        Ok(())
437    }
438
439    /// Test interpolation function APIs
440    pub fn test_interpolation_apis(&mut self) -> Result<()> {
441        self.test_zoom_api()?;
442        self.test_rotate_api()?;
443        self.test_affine_transform_api()?;
444        Ok(())
445    }
446
447    fn test_zoom_api(&mut self) -> Result<()> {
448        let mut incompatible_params = Vec::new();
449        let mut error_messages = Vec::new();
450
451        let input: Array2<f64> = Array2::zeros((10, 10));
452
453        // Test zoom with scalar factor
454        let scalar_zoom_test = scipy_compat_layer::scipy_ndimage::zoom(
455            input.view(),
456            vec![2.0f64, 2.0f64],
457            None,
458            None,
459            None,
460            None,
461            None,
462            None,
463        )
464        .is_ok();
465        if !scalar_zoom_test {
466            incompatible_params.push("zoom_factor".to_string());
467            error_messages.push("Zoom factor parameter handling differs".to_string());
468        }
469
470        // Test zoom with interpolation order
471        let order_test = scipy_compat_layer::scipy_ndimage::zoom(
472            input.view(),
473            vec![2.0f64, 2.0f64],
474            None,
475            Some(1), // Linear interpolation order
476            None,
477            None,
478            None,
479            None,
480        )
481        .is_ok();
482        if !order_test {
483            incompatible_params.push("interpolation_order".to_string());
484            error_messages.push("Interpolation order specification differs".to_string());
485        }
486
487        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 2.0);
488
489        self.results.push(ApiCompatibilityResult {
490            function_name: "zoom".to_string(),
491            test_case: "Zoom operation parameters".to_string(),
492            compatible: incompatible_params.is_empty(),
493            compatibility_score,
494            incompatible_parameters: incompatible_params,
495            error_messages,
496            suggestions: vec!["Verify zoom factor and order parameter compatibility".to_string()],
497            scipy_behavior: "Accepts scalar or array zoom factors, integer order".to_string(),
498            scirs2_behavior: "Uses array zoom factors and InterpolationOrder enum".to_string(),
499        });
500
501        Ok(())
502    }
503
504    fn test_rotate_api(&mut self) -> Result<()> {
505        let mut incompatible_params = Vec::new();
506        let mut error_messages = Vec::new();
507
508        let input: Array2<f64> = Array2::zeros((10, 10));
509
510        // Test basic rotation
511        let rotate_test = rotate(&input, 45.0, None, None, None, None, None, None).is_ok();
512        if !rotate_test {
513            incompatible_params.push("angle".to_string());
514            error_messages.push("Rotation angle parameter differs".to_string());
515        }
516
517        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 1.0);
518
519        self.results.push(ApiCompatibilityResult {
520            function_name: "rotate".to_string(),
521            test_case: "Rotation operation".to_string(),
522            compatible: incompatible_params.is_empty(),
523            compatibility_score,
524            incompatible_parameters: incompatible_params,
525            error_messages,
526            suggestions: vec!["Verify rotation parameter compatibility".to_string()],
527            scipy_behavior: "Accepts angle in degrees, various reshape options".to_string(),
528            scirs2_behavior: "Accepts angle in degrees with optional parameters".to_string(),
529        });
530
531        Ok(())
532    }
533
534    fn test_affine_transform_api(&mut self) -> Result<()> {
535        let mut incompatible_params = Vec::new();
536        let mut error_messages = Vec::new();
537
538        let input: Array2<f64> = Array2::zeros((10, 10));
539        let matrix = Array2::eye(2);
540
541        // Test affine transform
542        let affine_test =
543            affine_transform(&input, &matrix, None, None, None, None, None, None).is_ok();
544        if !affine_test {
545            incompatible_params.push("matrix".to_string());
546            error_messages.push("Affine matrix parameter handling differs".to_string());
547        }
548
549        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 1.0);
550
551        self.results.push(ApiCompatibilityResult {
552            function_name: "affine_transform".to_string(),
553            test_case: "Affine transformation".to_string(),
554            compatible: incompatible_params.is_empty(),
555            compatibility_score,
556            incompatible_parameters: incompatible_params,
557            error_messages,
558            suggestions: vec!["Verify affine matrix parameter format".to_string()],
559            scipy_behavior: "Accepts transformation matrix in specific format".to_string(),
560            scirs2_behavior: "Uses ndarray matrix for transformations".to_string(),
561        });
562
563        Ok(())
564    }
565
566    /// Test measurement function APIs
567    pub fn test_measurement_apis(&mut self) -> Result<()> {
568        self.test_center_of_mass_api()?;
569        self.test_label_api()?;
570        Ok(())
571    }
572
573    fn test_center_of_mass_api(&mut self) -> Result<()> {
574        let mut incompatible_params = Vec::new();
575        let mut error_messages = Vec::new();
576
577        let input = Array2::<f64>::ones((10, 10));
578
579        // Test center of mass calculation
580        let com_test = center_of_mass(&input).is_ok();
581        if !com_test {
582            incompatible_params.push("basic_functionality".to_string());
583            error_messages.push("center_of_mass basic functionality differs".to_string());
584        }
585
586        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 1.0);
587
588        self.results.push(ApiCompatibilityResult {
589            function_name: "center_of_mass".to_string(),
590            test_case: "Center of mass calculation".to_string(),
591            compatible: incompatible_params.is_empty(),
592            compatibility_score,
593            incompatible_parameters: incompatible_params,
594            error_messages,
595            suggestions: vec!["Verify center of mass calculation accuracy".to_string()],
596            scipy_behavior: "Returns center of mass coordinates".to_string(),
597            scirs2_behavior: "Returns coordinate array".to_string(),
598        });
599
600        Ok(())
601    }
602
603    fn test_label_api(&mut self) -> Result<()> {
604        let mut incompatible_params = Vec::new();
605        let mut error_messages = Vec::new();
606
607        let input = Array2::from_elem((10, 10), true);
608
609        // Test label function
610        let label_test = label(&input, None, None, None).is_ok();
611        if !label_test {
612            incompatible_params.push("structure".to_string());
613            error_messages.push("label structure parameter differs".to_string());
614        }
615
616        let compatibility_score = 1.0 - (incompatible_params.len() as f64 / 1.0);
617
618        self.results.push(ApiCompatibilityResult {
619            function_name: "label".to_string(),
620            test_case: "Connected component labeling".to_string(),
621            compatible: incompatible_params.is_empty(),
622            compatibility_score,
623            incompatible_parameters: incompatible_params,
624            error_messages,
625            suggestions: vec!["Verify labeling algorithm compatibility".to_string()],
626            scipy_behavior: "Connected component labeling with structure".to_string(),
627            scirs2_behavior: "Labeling with optional connectivity structure".to_string(),
628        });
629
630        Ok(())
631    }
632
633    /// Run all API compatibility tests
634    pub fn run_all_tests(&mut self) -> Result<()> {
635        println!("Running comprehensive API compatibility tests...");
636
637        self.test_filter_apis()?;
638        self.test_morphology_apis()?;
639        self.test_interpolation_apis()?;
640        self.test_measurement_apis()?;
641
642        // Calculate overall score
643        if !self.results.is_empty() {
644            self.overall_score = self
645                .results
646                .iter()
647                .map(|r| r.compatibility_score)
648                .sum::<f64>()
649                / self.results.len() as f64;
650        }
651
652        println!("API compatibility tests completed!");
653        Ok(())
654    }
655
656    /// Generate compatibility report
657    pub fn generate_report(&self) -> String {
658        let mut report = String::new();
659        report.push_str("# API Compatibility Report\n\n");
660
661        report.push_str(&format!(
662            "Overall Compatibility Score: {:.2}%\n\n",
663            self.overall_score * 100.0
664        ));
665
666        // Summary statistics
667        let total_tests = self.results.len();
668        let compatible_tests = self.results.iter().filter(|r| r.compatible).count();
669
670        report.push_str(&format!(
671            "Compatible Functions: {}/{} ({:.1}%)\n",
672            compatible_tests,
673            total_tests,
674            (compatible_tests as f64 / total_tests as f64) * 100.0
675        ));
676
677        report.push_str(&format!(
678            "Incompatible Functions: {}\n\n",
679            total_tests - compatible_tests
680        ));
681
682        // Detailed results
683        for result in &self.results {
684            report.push_str(&format!("## {}\n", result.function_name));
685            report.push_str(&format!(
686                "**Compatibility Score:** {:.2}%\n",
687                result.compatibility_score * 100.0
688            ));
689            report.push_str(&format!(
690                "**Compatible:** {}\n\n",
691                if result.compatible {
692                    "✓ Yes"
693                } else {
694                    "✗ No"
695                }
696            ));
697
698            if !result.incompatible_parameters.is_empty() {
699                report.push_str("**Incompatible Parameters:**\n");
700                for param in &result.incompatible_parameters {
701                    report.push_str(&format!("- {}\n", param));
702                }
703                report.push('\n');
704            }
705
706            if !result.error_messages.is_empty() {
707                report.push_str("**Issues:**\n");
708                for msg in &result.error_messages {
709                    report.push_str(&format!("- {}\n", msg));
710                }
711                report.push('\n');
712            }
713
714            if !result.suggestions.is_empty() {
715                report.push_str("**Suggestions:**\n");
716                for suggestion in &result.suggestions {
717                    report.push_str(&format!("- {}\n", suggestion));
718                }
719                report.push('\n');
720            }
721
722            report.push_str(&format!("**SciPy Behavior:** {}\n", result.scipy_behavior));
723            report.push_str(&format!(
724                "**scirs2 Behavior:** {}\n\n",
725                result.scirs2_behavior
726            ));
727
728            report.push_str("---\n\n");
729        }
730
731        report
732    }
733
734    /// Get test results
735    pub fn get_results(&self) -> &[ApiCompatibilityResult] {
736        &self.results
737    }
738
739    /// Get overall compatibility score
740    pub fn get_overall_score(&self) -> f64 {
741        self.overall_score
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn test_compatibility_tester_creation() {
751        let tester = ApiCompatibilityTester::new();
752        assert_eq!(tester.results.len(), 0);
753        assert_eq!(tester.overall_score, 0.0);
754    }
755
756    #[test]
757    fn test_compatibility_config() {
758        let config = CompatibilityConfig::default();
759        assert!(config.test_edge_cases);
760        assert!(config.test_error_conditions);
761        assert!(!config.test_performance);
762    }
763
764    #[test]
765    fn test_api_result_creation() {
766        let result = ApiCompatibilityResult {
767            function_name: "test_function".to_string(),
768            test_case: "basic_test".to_string(),
769            compatible: true,
770            compatibility_score: 1.0,
771            incompatible_parameters: vec![],
772            error_messages: vec![],
773            suggestions: vec![],
774            scipy_behavior: "Expected behavior".to_string(),
775            scirs2_behavior: "Actual behavior".to_string(),
776        };
777
778        assert!(result.compatible);
779        assert_eq!(result.compatibility_score, 1.0);
780        assert_eq!(result.function_name, "test_function");
781    }
782}