1use crate::error::Result;
41use crate::traits::{Estimator, Fit, Predict, PredictProba, Transform};
42use 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
50type OwnedMatrix = ArrayBase<OwnedRepr<f64>, Ix2, f64>;
66type OwnedVector = ArrayBase<OwnedRepr<f64>, Ix1, f64>;
67
68#[derive(Debug)]
70pub struct ContractTester {
71 config: ContractTestConfig,
72 results: Vec<ContractTestResult>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ContractTestConfig {
78 pub property_test_cases: usize,
80 pub test_timeout: Duration,
82 pub include_performance_tests: bool,
84 pub random_seed: u64,
86 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 pub fn new() -> Self {
105 Self::with_config(ContractTestConfig::default())
106 }
107
108 pub fn with_config(config: ContractTestConfig) -> Self {
110 Self {
111 config,
112 results: Vec::new(),
113 }
114 }
115
116 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 self.test_config_immutability(estimator, &mut test_result)?;
127
128 self.test_fit_consistency(estimator, &mut test_result)?;
130
131 self.test_prediction_shape_consistency(estimator, &mut test_result)?;
133
134 self.test_error_handling_contracts(estimator, &mut test_result)?;
136
137 self.test_memory_safety_contracts(estimator, &mut test_result)?;
139
140 if self.config.include_performance_tests {
141 self.test_performance_contracts(estimator, &mut test_result)?;
143 }
144
145 self.results.push(test_result);
146 Ok(())
147 }
148
149 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 self.test_transform_consistency(transformer, &mut test_result)?;
161
162 self.test_fit_requirement(transformer, &mut test_result)?;
164
165 self.test_shape_transformation_contract(transformer, &mut test_result)?;
167
168 self.test_inverse_transform_properties(transformer, &mut test_result)?;
170
171 self.results.push(test_result);
172 Ok(())
173 }
174
175 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 self.test_probability_sum_constraint(predictor, &mut test_result)?;
185
186 self.test_probability_bounds(predictor, &mut test_result)?;
188
189 self.test_predict_proba_consistency(predictor, &mut test_result)?;
191
192 self.results.push(test_result);
193 Ok(())
194 }
195
196 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 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 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 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 let _config1 = estimator.config();
298 let _config2 = estimator.config();
299
300 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 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 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 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 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 let x_mismatch = Array2::zeros((10, 5));
432 let y_mismatch = Array1::zeros(15); 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 let x_empty = Array2::zeros((0, 5));
441 let y_empty = Array1::zeros(0);
442
443 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; 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 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 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 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 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 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 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 let transform1 = fitted.transform(&x)?;
562 let transform2 = fitted.transform(&x)?;
563
564 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 let x = Array2::zeros((10, 5));
603
604 match transformer.transform(&x) {
605 Ok(_) => {
606 }
609 Err(_) => {
610 }
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 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; 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 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 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; 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#[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#[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#[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#[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
854pub struct TraitLaws;
856
857impl TraitLaws {
858 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 Ok(true)
870 }
871
872 pub fn test_monad_laws<E>(_estimator: &E) -> Result<bool>
874 where
875 E: Estimator,
876 {
877 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}