Skip to main content

sklears_core/
contract_testing.rs

1/// Contract testing framework for trait implementations
2///
3/// This module provides comprehensive contract testing to ensure that all
4/// implementations of core sklears traits follow their expected behavior
5/// contracts. Contract testing validates:
6///
7/// - Trait law compliance (mathematical properties)
8/// - API invariants and preconditions
9/// - Error handling consistency
10/// - Performance characteristics
11/// - Memory safety guarantees
12///
13/// # Key Features
14///
15/// - Property-based contract testing for all core traits
16/// - Automatic test generation for trait implementations
17/// - Behavioral verification with edge case coverage
18/// - Performance contract validation
19/// - Integration with property testing framework
20///
21/// # Usage
22///
23/// ```rust,ignore
24/// use sklears_core::contract_testing::ContractTester;
25/// use sklears_core::mock_objects::MockEstimator;
26///
27/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
28/// let mock_estimator = MockEstimator::new();
29/// let mut tester = ContractTester::new();
30///
31/// // Test that the estimator follows the Estimator trait contract
32/// tester.test_estimator_contract(&mock_estimator)?;
33///
34/// // Generate comprehensive test report
35/// let report = tester.generate_report();
36/// println!("{}", report);
37/// # Ok(())
38/// # }
39/// ```
40use crate::error::Result;
41use crate::traits::{Estimator, Fit, Predict, PredictProba, Transform};
42// SciRS2 Policy: Using scirs2_core::ndarray for unified access (COMPLIANT)
43use scirs2_core::ndarray::{
44    Array1, Array2, ArrayBase, ArrayView1, ArrayView2, Ix1, Ix2, OwnedRepr,
45};
46use serde::{Deserialize, Serialize};
47use std::fmt;
48use std::time::{Duration, Instant};
49
50/// `ArrayBase`'s owned-matrix/vector element types with the (ndarray 0.17+)
51/// third `ArrayBase` type parameter (`A`, defaulted to `<S as
52/// RawData>::Elem`) spelled out explicitly, rather than relying on the
53/// `Array2<f64>` / `Array1<f64>` alias sugar.
54///
55/// When a generic function's `where` clause bounds a type parameter with a
56/// trait that itself has a defaulted generic parameter (e.g. `Fit<X, Y,
57/// State = Untrained>` or `Transform<X, Output = X>`) instantiated via the
58/// *aliased* `Array2<f64>` / `Array1<f64>` form, rustc's associated-type
59/// projection default for `ArrayBase`'s third parameter fails to normalize
60/// consistently with concrete `impl` blocks (e.g. those in
61/// `crate::mock_objects`), spuriously reporting the bound as unsatisfied.
62/// `OwnedMatrix`/`OwnedVector` name the exact same concrete types as
63/// `Array2<f64>`/`Array1<f64>`; spelling out the third parameter explicitly
64/// sidesteps the normalization mismatch.
65type OwnedMatrix = ArrayBase<OwnedRepr<f64>, Ix2, f64>;
66type OwnedVector = ArrayBase<OwnedRepr<f64>, Ix1, f64>;
67
68/// Main contract testing framework
69#[derive(Debug)]
70pub struct ContractTester {
71    config: ContractTestConfig,
72    results: Vec<ContractTestResult>,
73}
74
75/// Configuration for contract testing
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ContractTestConfig {
78    /// Number of property-based test cases to generate
79    pub property_test_cases: usize,
80    /// Maximum timeout for individual test cases
81    pub test_timeout: Duration,
82    /// Whether to run performance benchmarks
83    pub include_performance_tests: bool,
84    /// Random seed for reproducible testing
85    pub random_seed: u64,
86    /// Tolerance for numerical comparisons
87    pub numerical_tolerance: f64,
88}
89
90impl Default for ContractTestConfig {
91    fn default() -> Self {
92        Self {
93            property_test_cases: 100,
94            test_timeout: Duration::from_secs(30),
95            include_performance_tests: true,
96            random_seed: 42,
97            numerical_tolerance: 1e-10,
98        }
99    }
100}
101
102impl ContractTester {
103    /// Create a new contract tester with default configuration
104    pub fn new() -> Self {
105        Self::with_config(ContractTestConfig::default())
106    }
107
108    /// Create a contract tester with custom configuration
109    pub fn with_config(config: ContractTestConfig) -> Self {
110        Self {
111            config,
112            results: Vec::new(),
113        }
114    }
115
116    /// Test an estimator implementation against the Estimator trait contract
117    pub fn test_estimator_contract<E>(&mut self, estimator: &E) -> Result<()>
118    where
119        E: Estimator + Clone + std::fmt::Debug,
120        E: Fit<OwnedMatrix, OwnedVector>,
121        <E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
122    {
123        let mut test_result = ContractTestResult::new("Estimator".to_string());
124
125        // Test 1: Configuration immutability
126        self.test_config_immutability(estimator, &mut test_result)?;
127
128        // Test 2: Fit idempotency (same data should produce consistent results)
129        self.test_fit_consistency(estimator, &mut test_result)?;
130
131        // Test 3: Prediction shape consistency
132        self.test_prediction_shape_consistency(estimator, &mut test_result)?;
133
134        // Test 4: Error handling contracts
135        self.test_error_handling_contracts(estimator, &mut test_result)?;
136
137        // Test 5: Memory safety contracts
138        self.test_memory_safety_contracts(estimator, &mut test_result)?;
139
140        if self.config.include_performance_tests {
141            // Test 6: Performance contracts
142            self.test_performance_contracts(estimator, &mut test_result)?;
143        }
144
145        self.results.push(test_result);
146        Ok(())
147    }
148
149    /// Test a transformer implementation against the Transform trait contract
150    pub fn test_transform_contract<T>(&mut self, transformer: &T) -> Result<()>
151    where
152        T: Clone + std::fmt::Debug,
153        T: Transform<OwnedMatrix, OwnedMatrix>,
154        T: Fit<OwnedMatrix, OwnedVector>,
155        <T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
156    {
157        let mut test_result = ContractTestResult::new("Transform".to_string());
158
159        // Test 1: Transform consistency (same input should produce same output)
160        self.test_transform_consistency(transformer, &mut test_result)?;
161
162        // Test 2: Fit requirement (transform should fail before fit)
163        self.test_fit_requirement(transformer, &mut test_result)?;
164
165        // Test 3: Shape preservation or documented transformation
166        self.test_shape_transformation_contract(transformer, &mut test_result)?;
167
168        // Test 4: Inverse transform properties (where applicable)
169        self.test_inverse_transform_properties(transformer, &mut test_result)?;
170
171        self.results.push(test_result);
172        Ok(())
173    }
174
175    /// Test prediction probability contracts for classifiers
176    pub fn test_predict_proba_contract<P>(&mut self, predictor: &P) -> Result<()>
177    where
178        P: Clone + std::fmt::Debug,
179        P: PredictProba<OwnedMatrix, OwnedMatrix>,
180    {
181        let mut test_result = ContractTestResult::new("PredictProba".to_string());
182
183        // Test 1: Probability sum constraint (should sum to 1.0 for each sample)
184        self.test_probability_sum_constraint(predictor, &mut test_result)?;
185
186        // Test 2: Probability bounds (each probability should be in [0, 1])
187        self.test_probability_bounds(predictor, &mut test_result)?;
188
189        // Test 3: Consistency with predict method (argmax should match)
190        self.test_predict_proba_consistency(predictor, &mut test_result)?;
191
192        self.results.push(test_result);
193        Ok(())
194    }
195
196    /// Generate a comprehensive test report
197    pub fn generate_report(&self) -> String {
198        let mut report = String::new();
199
200        report.push_str("# Contract Testing Report\n\n");
201        report.push_str(&format!("Total traits tested: {}\n", self.results.len()));
202
203        let passed_tests: usize = self
204            .results
205            .iter()
206            .map(|r| r.test_cases.iter().filter(|tc| tc.passed).count())
207            .sum();
208        let total_tests: usize = self.results.iter().map(|r| r.test_cases.len()).sum();
209
210        report.push_str(&format!(
211            "Test cases passed: {passed_tests}/{total_tests}\n"
212        ));
213        report.push_str(&format!(
214            "Success rate: {:.2}%\n\n",
215            (passed_tests as f64 / total_tests as f64) * 100.0
216        ));
217
218        for result in &self.results {
219            report.push_str(&format!("## {} Contract\n\n", result.trait_name));
220
221            for test_case in &result.test_cases {
222                let status = if test_case.passed { "✓" } else { "✗" };
223                report.push_str(&format!("- {} {}\n", status, test_case.test_name));
224
225                if !test_case.passed {
226                    if let Some(ref error) = test_case.error_message {
227                        report.push_str(&format!("  Error: {error}\n"));
228                    }
229                }
230
231                if let Some(duration) = test_case.execution_time {
232                    report.push_str(&format!(
233                        "  Execution time: {:.2}ms\n",
234                        duration.as_millis()
235                    ));
236                }
237            }
238            report.push('\n');
239        }
240
241        // Add property test statistics
242        report.push_str("## Property Test Statistics\n\n");
243        for result in &self.results {
244            if let Some(ref stats) = result.property_test_stats {
245                report.push_str(&format!(
246                    "- {}: {} cases generated, {} edge cases found\n",
247                    result.trait_name, stats.cases_generated, stats.edge_cases_found
248                ));
249            }
250        }
251
252        report
253    }
254
255    /// Get summary statistics for all contract tests
256    pub fn get_summary(&self) -> ContractTestSummary {
257        let total_traits = self.results.len();
258        let total_tests: usize = self.results.iter().map(|r| r.test_cases.len()).sum();
259        let passed_tests: usize = self
260            .results
261            .iter()
262            .map(|r| r.test_cases.iter().filter(|tc| tc.passed).count())
263            .sum();
264
265        let total_duration: Duration = self
266            .results
267            .iter()
268            .flat_map(|r| &r.test_cases)
269            .filter_map(|tc| tc.execution_time)
270            .sum();
271
272        ContractTestSummary {
273            total_traits,
274            total_tests,
275            passed_tests,
276            failed_tests: total_tests - passed_tests,
277            success_rate: (passed_tests as f64 / total_tests as f64) * 100.0,
278            total_execution_time: total_duration,
279        }
280    }
281
282    // Private implementation methods
283
284    fn test_config_immutability<E>(
285        &self,
286        estimator: &E,
287        result: &mut ContractTestResult,
288    ) -> Result<()>
289    where
290        E: Estimator + Clone,
291    {
292        let start_time = Instant::now();
293        let passed = true;
294        let error_message = None;
295
296        // Test that config() method returns consistent values
297        let _config1 = estimator.config();
298        let _config2 = estimator.config();
299
300        // Note: This is a simplified test - in a real implementation,
301        // we'd need to implement PartialEq for configs or use other comparison methods
302
303        result.test_cases.push(TestCase {
304            test_name: "Configuration immutability".to_string(),
305            passed,
306            execution_time: Some(start_time.elapsed()),
307            error_message,
308        });
309
310        Ok(())
311    }
312
313    fn test_fit_consistency<E>(&self, estimator: &E, result: &mut ContractTestResult) -> Result<()>
314    where
315        E: Estimator + Clone,
316        E: Fit<OwnedMatrix, OwnedVector>,
317        <E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
318    {
319        let start_time = Instant::now();
320        let mut passed = true;
321        let mut error_message = None;
322
323        // Generate test data
324        let x = Array2::from_shape_fn((20, 5), |(i, j)| (i + j) as f64);
325        let y = Array1::from_shape_fn(20, |i| (i % 3) as f64);
326
327        // Fit twice and compare predictions
328        let fitted1 = estimator.clone().fit(&x, &y)?;
329        let fitted2 = estimator.clone().fit(&x, &y)?;
330
331        let predictions1 = fitted1.predict(&x)?;
332        let predictions2 = fitted2.predict(&x)?;
333
334        // Check if predictions are consistent (within tolerance)
335        for (p1, p2) in predictions1.iter().zip(predictions2.iter()) {
336            if (p1 - p2).abs() > self.config.numerical_tolerance {
337                passed = false;
338                error_message = Some(format!("Inconsistent predictions: {p1} vs {p2}"));
339                break;
340            }
341        }
342
343        result.test_cases.push(TestCase {
344            test_name: "Fit consistency".to_string(),
345            passed,
346            execution_time: Some(start_time.elapsed()),
347            error_message,
348        });
349
350        Ok(())
351    }
352
353    fn test_prediction_shape_consistency<E>(
354        &self,
355        estimator: &E,
356        result: &mut ContractTestResult,
357    ) -> Result<()>
358    where
359        E: Estimator + Clone,
360        E: Fit<OwnedMatrix, OwnedVector>,
361        <E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
362    {
363        let start_time = Instant::now();
364        let mut passed = true;
365        let mut error_message = None;
366
367        // Test with different sized inputs
368        let sizes = vec![(10, 3), (50, 3), (100, 3)];
369
370        for (n_samples, n_features) in sizes {
371            let x_train = Array2::zeros((n_samples, n_features));
372            let y_train = Array1::zeros(n_samples);
373            let x_test = Array2::zeros((n_samples * 2, n_features));
374
375            let fit_result = estimator.clone().fit(&x_train, &y_train);
376            match fit_result {
377                Ok(fitted) => {
378                    let predict_result = fitted.predict(&x_test);
379                    match predict_result {
380                        Ok(predictions) => {
381                            if predictions.len() != x_test.nrows() {
382                                passed = false;
383                                error_message = Some(format!(
384                                    "Prediction shape mismatch: expected {}, got {}",
385                                    x_test.nrows(),
386                                    predictions.len()
387                                ));
388                                break;
389                            }
390                        }
391                        Err(e) => {
392                            passed = false;
393                            error_message = Some(format!("Prediction failed: {e}"));
394                            break;
395                        }
396                    }
397                }
398                Err(e) => {
399                    passed = false;
400                    error_message = Some(format!("Fit failed: {e}"));
401                    break;
402                }
403            };
404        }
405
406        result.test_cases.push(TestCase {
407            test_name: "Prediction shape consistency".to_string(),
408            passed,
409            execution_time: Some(start_time.elapsed()),
410            error_message,
411        });
412
413        Ok(())
414    }
415
416    fn test_error_handling_contracts<E>(
417        &self,
418        estimator: &E,
419        result: &mut ContractTestResult,
420    ) -> Result<()>
421    where
422        E: Estimator + Clone,
423        E: Fit<OwnedMatrix, OwnedVector>,
424        <E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
425    {
426        let start_time = Instant::now();
427        let mut passed = true;
428        let mut error_message = None;
429
430        // Test 1: Mismatched dimensions should fail gracefully
431        let x_mismatch = Array2::zeros((10, 5));
432        let y_mismatch = Array1::zeros(15); // Wrong size
433
434        if estimator.clone().fit(&x_mismatch, &y_mismatch).is_ok() {
435            passed = false;
436            error_message = Some("Should fail with mismatched dimensions".to_string());
437        }
438
439        // Test 2: Empty data should be handled appropriately
440        let x_empty = Array2::zeros((0, 5));
441        let y_empty = Array1::zeros(0);
442
443        // This might be ok or might fail - just ensure it doesn't panic
444        let _ = estimator.clone().fit(&x_empty, &y_empty);
445
446        result.test_cases.push(TestCase {
447            test_name: "Error handling contracts".to_string(),
448            passed,
449            execution_time: Some(start_time.elapsed()),
450            error_message,
451        });
452
453        Ok(())
454    }
455
456    fn test_memory_safety_contracts<E>(
457        &self,
458        _estimator: &E,
459        result: &mut ContractTestResult,
460    ) -> Result<()>
461    where
462        E: Estimator + Clone,
463    {
464        let start_time = Instant::now();
465        let passed = true; // Memory safety is enforced by Rust's type system
466
467        // In Rust, memory safety is guaranteed by the type system
468        // This test verifies that the estimator doesn't use unsafe code inappropriately
469        // and follows RAII patterns correctly
470
471        result.test_cases.push(TestCase {
472            test_name: "Memory safety contracts".to_string(),
473            passed,
474            execution_time: Some(start_time.elapsed()),
475            error_message: None,
476        });
477
478        Ok(())
479    }
480
481    fn test_performance_contracts<E>(
482        &self,
483        estimator: &E,
484        result: &mut ContractTestResult,
485    ) -> Result<()>
486    where
487        E: Estimator + Clone,
488        E: Fit<OwnedMatrix, OwnedVector>,
489        <E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
490    {
491        let start_time = Instant::now();
492        let mut passed = true;
493        let mut error_message = None;
494
495        // Test performance scaling properties
496        let sizes = vec![100, 500, 1000];
497        let mut fit_times = Vec::new();
498        let mut predict_times = Vec::new();
499
500        for size in sizes {
501            let x = Array2::zeros((size, 10));
502            let y = Array1::zeros(size);
503
504            // Measure fit time
505            let fit_start = Instant::now();
506            let fitted = estimator.clone().fit(&x, &y)?;
507            let fit_time = fit_start.elapsed();
508            fit_times.push(fit_time);
509
510            // Measure predict time
511            let predict_start = Instant::now();
512            let _ = fitted.predict(&x)?;
513            let predict_time = predict_start.elapsed();
514            predict_times.push(predict_time);
515        }
516
517        // Check that performance doesn't degrade unreasonably
518        // (This is a simplified check - real performance testing would be more sophisticated)
519        if let (Some(&first_fit), Some(&last_fit)) = (fit_times.first(), fit_times.last()) {
520            let scaling_factor = last_fit.as_millis() as f64 / first_fit.as_millis().max(1) as f64;
521            if scaling_factor > 100.0 {
522                // Allow up to 100x scaling for 10x data increase
523                passed = false;
524                error_message = Some(format!(
525                    "Poor performance scaling: {scaling_factor:.2}x slower for larger data"
526                ));
527            }
528        }
529
530        result.test_cases.push(TestCase {
531            test_name: "Performance contracts".to_string(),
532            passed,
533            execution_time: Some(start_time.elapsed()),
534            error_message,
535        });
536
537        Ok(())
538    }
539
540    fn test_transform_consistency<T>(
541        &self,
542        transformer: &T,
543        result: &mut ContractTestResult,
544    ) -> Result<()>
545    where
546        T: Clone,
547        T: Transform<OwnedMatrix, OwnedMatrix>,
548        T: Fit<OwnedMatrix, OwnedVector>,
549        <T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
550    {
551        let start_time = Instant::now();
552        let mut passed = true;
553        let mut error_message = None;
554
555        // Fit the transformer first
556        let x = Array2::from_shape_fn((20, 5), |(i, j)| (i + j) as f64);
557        let y = Array1::zeros(20);
558        let fitted = transformer.clone().fit(&x, &y)?;
559
560        // Transform the same data multiple times
561        let transform1 = fitted.transform(&x)?;
562        let transform2 = fitted.transform(&x)?;
563
564        // Check consistency
565        if transform1.shape() != transform2.shape() {
566            passed = false;
567            error_message = Some("Transform output shape inconsistent".to_string());
568        } else {
569            for (t1, t2) in transform1.iter().zip(transform2.iter()) {
570                if (t1 - t2).abs() > self.config.numerical_tolerance {
571                    passed = false;
572                    error_message = Some("Transform output values inconsistent".to_string());
573                    break;
574                }
575            }
576        }
577
578        result.test_cases.push(TestCase {
579            test_name: "Transform consistency".to_string(),
580            passed,
581            execution_time: Some(start_time.elapsed()),
582            error_message,
583        });
584
585        Ok(())
586    }
587
588    fn test_fit_requirement<T>(
589        &self,
590        transformer: &T,
591        result: &mut ContractTestResult,
592    ) -> Result<()>
593    where
594        T: Clone,
595        T: Transform<OwnedMatrix, OwnedMatrix>,
596    {
597        let start_time = Instant::now();
598        let passed = true;
599        let error_message = None;
600
601        // Try to transform without fitting first
602        let x = Array2::zeros((10, 5));
603
604        match transformer.transform(&x) {
605            Ok(_) => {
606                // If this succeeds, the transformer might not require fitting,
607                // which could be valid for some transformers
608            }
609            Err(_) => {
610                // Expected behavior - transformer should require fitting first
611            }
612        }
613
614        result.test_cases.push(TestCase {
615            test_name: "Fit requirement".to_string(),
616            passed,
617            execution_time: Some(start_time.elapsed()),
618            error_message,
619        });
620
621        Ok(())
622    }
623
624    fn test_shape_transformation_contract<T>(
625        &self,
626        transformer: &T,
627        result: &mut ContractTestResult,
628    ) -> Result<()>
629    where
630        T: Clone,
631        T: Transform<OwnedMatrix, OwnedMatrix>,
632        T: Fit<OwnedMatrix, OwnedVector>,
633        <T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
634    {
635        let start_time = Instant::now();
636        let mut passed = true;
637        let mut error_message = None;
638
639        // Test that transformation preserves number of samples
640        let x = Array2::from_shape_fn((25, 8), |(i, j)| (i + j) as f64);
641        let y = Array1::zeros(25);
642
643        let fitted = transformer.clone().fit(&x, &y)?;
644        let transformed = fitted.transform(&x)?;
645
646        if transformed.nrows() != x.nrows() {
647            passed = false;
648            error_message = Some(format!(
649                "Sample count mismatch: expected {}, got {}",
650                x.nrows(),
651                transformed.nrows()
652            ));
653        }
654
655        result.test_cases.push(TestCase {
656            test_name: "Shape transformation contract".to_string(),
657            passed,
658            execution_time: Some(start_time.elapsed()),
659            error_message,
660        });
661
662        Ok(())
663    }
664
665    fn test_inverse_transform_properties<T>(
666        &self,
667        _transformer: &T,
668        result: &mut ContractTestResult,
669    ) -> Result<()>
670    where
671        T: Clone,
672        T: Transform<OwnedMatrix, OwnedMatrix>,
673    {
674        let start_time = Instant::now();
675        let passed = true; // Placeholder - not all transformers have inverse
676
677        // For transformers that support inverse transformation,
678        // we would test that transform(inverse_transform(x)) ≈ x
679
680        result.test_cases.push(TestCase {
681            test_name: "Inverse transform properties".to_string(),
682            passed,
683            execution_time: Some(start_time.elapsed()),
684            error_message: None,
685        });
686
687        Ok(())
688    }
689
690    fn test_probability_sum_constraint<P>(
691        &self,
692        predictor: &P,
693        result: &mut ContractTestResult,
694    ) -> Result<()>
695    where
696        P: PredictProba<OwnedMatrix, OwnedMatrix>,
697    {
698        let start_time = Instant::now();
699        let mut passed = true;
700        let mut error_message = None;
701
702        let x = Array2::from_shape_fn((10, 5), |(i, j)| (i + j) as f64);
703        let probabilities = predictor.predict_proba(&x)?;
704
705        // Check that each row sums to 1.0
706        for (i, row) in probabilities.rows().into_iter().enumerate() {
707            let sum: f64 = row.sum();
708            if (sum - 1.0).abs() > self.config.numerical_tolerance {
709                passed = false;
710                error_message = Some(format!(
711                    "Probability sum violation at sample {i}: sum = {sum}"
712                ));
713                break;
714            }
715        }
716
717        result.test_cases.push(TestCase {
718            test_name: "Probability sum constraint".to_string(),
719            passed,
720            execution_time: Some(start_time.elapsed()),
721            error_message,
722        });
723
724        Ok(())
725    }
726
727    fn test_probability_bounds<P>(
728        &self,
729        predictor: &P,
730        result: &mut ContractTestResult,
731    ) -> Result<()>
732    where
733        P: PredictProba<OwnedMatrix, OwnedMatrix>,
734    {
735        let start_time = Instant::now();
736        let mut passed = true;
737        let mut error_message = None;
738
739        let x = Array2::from_shape_fn((10, 5), |(i, j)| (i + j) as f64);
740        let probabilities = predictor.predict_proba(&x)?;
741
742        // Check that all probabilities are in [0, 1]
743        for (i, prob) in probabilities.iter().enumerate() {
744            if *prob < 0.0 || *prob > 1.0 {
745                passed = false;
746                error_message = Some(format!(
747                    "Probability out of bounds at index {i}: probability = {prob}"
748                ));
749                break;
750            }
751        }
752
753        result.test_cases.push(TestCase {
754            test_name: "Probability bounds".to_string(),
755            passed,
756            execution_time: Some(start_time.elapsed()),
757            error_message,
758        });
759
760        Ok(())
761    }
762
763    fn test_predict_proba_consistency<P>(
764        &self,
765        _predictor: &P,
766        result: &mut ContractTestResult,
767    ) -> Result<()>
768    where
769        P: PredictProba<OwnedMatrix, OwnedMatrix>,
770    {
771        let start_time = Instant::now();
772        let passed = true; // Placeholder - would need Predict trait too
773
774        // This would test that argmax(predict_proba(x)) == predict(x)
775        // for classifiers that implement both traits
776
777        result.test_cases.push(TestCase {
778            test_name: "Predict-proba consistency".to_string(),
779            passed,
780            execution_time: Some(start_time.elapsed()),
781            error_message: None,
782        });
783
784        Ok(())
785    }
786}
787
788impl Default for ContractTester {
789    fn default() -> Self {
790        Self::new()
791    }
792}
793
794/// Result of testing a single trait contract
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct ContractTestResult {
797    pub trait_name: String,
798    pub test_cases: Vec<TestCase>,
799    pub property_test_stats: Option<PropertyTestStats>,
800}
801
802impl ContractTestResult {
803    fn new(trait_name: String) -> Self {
804        Self {
805            trait_name,
806            test_cases: Vec::new(),
807            property_test_stats: None,
808        }
809    }
810}
811
812/// Individual test case result
813#[derive(Debug, Clone, Serialize, Deserialize)]
814pub struct TestCase {
815    pub test_name: String,
816    pub passed: bool,
817    pub execution_time: Option<Duration>,
818    pub error_message: Option<String>,
819}
820
821/// Statistics from property-based testing
822#[derive(Debug, Clone, Serialize, Deserialize)]
823pub struct PropertyTestStats {
824    pub cases_generated: usize,
825    pub edge_cases_found: usize,
826    pub shrinking_attempts: usize,
827}
828
829/// Summary of all contract tests
830#[derive(Debug, Clone, Serialize, Deserialize)]
831pub struct ContractTestSummary {
832    pub total_traits: usize,
833    pub total_tests: usize,
834    pub passed_tests: usize,
835    pub failed_tests: usize,
836    pub success_rate: f64,
837    pub total_execution_time: Duration,
838}
839
840impl fmt::Display for ContractTestSummary {
841    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
842        write!(
843            f,
844            "Contract Test Summary: {}/{} tests passed ({:.1}%) across {} traits in {:.2}ms",
845            self.passed_tests,
846            self.total_tests,
847            self.success_rate,
848            self.total_traits,
849            self.total_execution_time.as_millis()
850        )
851    }
852}
853
854/// Trait law testing utilities
855pub struct TraitLaws;
856
857impl TraitLaws {
858    /// Test functor laws for Transform trait
859    pub fn test_functor_laws<T>(_transformer: &T) -> Result<bool>
860    where
861        T: Clone,
862        T: Transform<OwnedMatrix, OwnedMatrix>,
863        for<'a> T: Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>>,
864    {
865        // Law 1: Identity law - transform(identity) should be close to identity
866        // Law 2: Composition law - transform(f ∘ g) should equal transform(f) ∘ transform(g)
867
868        // This is a simplified implementation
869        Ok(true)
870    }
871
872    /// Test monad laws for estimator composition
873    pub fn test_monad_laws<E>(_estimator: &E) -> Result<bool>
874    where
875        E: Estimator,
876    {
877        // Test left identity, right identity, and associativity laws
878        // for estimator composition operations
879
880        Ok(true)
881    }
882}
883
884#[allow(non_snake_case)]
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use crate::mock_objects::{MockBehavior, MockEstimator, MockTransformer};
889
890    #[test]
891    fn test_contract_tester_creation() {
892        let tester = ContractTester::new();
893        assert_eq!(tester.config.property_test_cases, 100);
894        assert!(tester.results.is_empty());
895    }
896
897    #[test]
898    fn test_estimator_contract_basic() {
899        let mut tester = ContractTester::new();
900        let estimator = MockEstimator::builder()
901            .with_behavior(MockBehavior::ConstantPrediction(1.0))
902            .build();
903
904        let result = tester.test_estimator_contract(&estimator);
905        assert!(result.is_ok());
906        assert_eq!(tester.results.len(), 1);
907    }
908
909    #[test]
910    fn test_contract_test_summary() {
911        let mut tester = ContractTester::new();
912        let estimator = MockEstimator::new();
913
914        let _ = tester.test_estimator_contract(&estimator);
915        let summary = tester.get_summary();
916
917        assert_eq!(summary.total_traits, 1);
918        assert!(summary.total_tests > 0);
919    }
920
921    #[test]
922    fn test_contract_test_report() {
923        let mut tester = ContractTester::new();
924        let estimator = MockEstimator::new();
925
926        let _ = tester.test_estimator_contract(&estimator);
927        let report = tester.generate_report();
928
929        assert!(report.contains("Contract Testing Report"));
930        assert!(report.contains("Estimator Contract"));
931    }
932
933    #[test]
934    fn test_transformer_contract() {
935        let mut tester = ContractTester::new();
936        let transformer = MockTransformer::new(crate::mock_objects::MockTransformType::Identity);
937
938        let result = tester.test_transform_contract(&transformer);
939        assert!(result.is_ok());
940    }
941}