1type 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#[derive(Debug, Clone)]
18pub struct ApiCompatibilityResult {
19 pub function_name: String,
21 pub test_case: String,
23 pub compatible: bool,
25 pub compatibility_score: f64,
27 pub incompatible_parameters: Vec<String>,
29 pub error_messages: Vec<String>,
31 pub suggestions: Vec<String>,
33 pub scipy_behavior: String,
35 pub scirs2_behavior: String,
37}
38
39#[derive(Debug, Clone)]
41pub struct ParameterTest {
42 pub name: String,
44 pub description: String,
46 pub test_fn: fn() -> (bool, Option<String>),
48 pub scipy_expected: String,
50 pub priority: String,
52}
53
54pub struct ApiCompatibilityTester {
56 results: Vec<ApiCompatibilityResult>,
58 overall_score: f64,
60 config: CompatibilityConfig,
62}
63
64#[derive(Debug, Clone)]
66pub struct CompatibilityConfig {
67 pub test_edge_cases: bool,
69 pub test_error_conditions: bool,
71 pub test_performance: bool,
73 pub numerical_tolerance: f64,
75 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, numerical_tolerance: 1e-10,
86 max_test_size: 1000,
87 }
88 }
89}
90
91impl ApiCompatibilityTester {
92 pub fn new() -> Self {
94 Self::with_config(CompatibilityConfig::default())
95 }
96
97 pub fn with_config(config: CompatibilityConfig) -> Self {
99 Self {
100 results: Vec::new(),
101 overall_score: 0.0,
102 config,
103 }
104 }
105
106 pub fn test_filter_apis(&mut self) -> Result<()> {
108 self.test_gaussian_filter_api()?;
110
111 self.test_median_filter_api()?;
113
114 self.test_uniform_filter_api()?;
116
117 self.test_sobel_filter_api()?;
119
120 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 let input: Array2<f64> = Array2::zeros((10, 10));
133
134 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 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 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 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 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 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); 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 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 suggestions.push("Use size parameter instead of footprint for filter kernel".to_string());
208
209 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let order_test = scipy_compat_layer::scipy_ndimage::zoom(
472 input.view(),
473 vec![2.0f64, 2.0f64],
474 None,
475 Some(1), 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 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 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 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 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 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 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 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 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 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 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 pub fn get_results(&self) -> &[ApiCompatibilityResult] {
736 &self.results
737 }
738
739 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}