use crate::error::Result;
use crate::traits::{Estimator, Fit, Predict, PredictProba, Transform};
use scirs2_core::ndarray::{
Array1, Array2, ArrayBase, ArrayView1, ArrayView2, Ix1, Ix2, OwnedRepr,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::{Duration, Instant};
type OwnedMatrix = ArrayBase<OwnedRepr<f64>, Ix2, f64>;
type OwnedVector = ArrayBase<OwnedRepr<f64>, Ix1, f64>;
#[derive(Debug)]
pub struct ContractTester {
config: ContractTestConfig,
results: Vec<ContractTestResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractTestConfig {
pub property_test_cases: usize,
pub test_timeout: Duration,
pub include_performance_tests: bool,
pub random_seed: u64,
pub numerical_tolerance: f64,
}
impl Default for ContractTestConfig {
fn default() -> Self {
Self {
property_test_cases: 100,
test_timeout: Duration::from_secs(30),
include_performance_tests: true,
random_seed: 42,
numerical_tolerance: 1e-10,
}
}
}
impl ContractTester {
pub fn new() -> Self {
Self::with_config(ContractTestConfig::default())
}
pub fn with_config(config: ContractTestConfig) -> Self {
Self {
config,
results: Vec::new(),
}
}
pub fn test_estimator_contract<E>(&mut self, estimator: &E) -> Result<()>
where
E: Estimator + Clone + std::fmt::Debug,
E: Fit<OwnedMatrix, OwnedVector>,
<E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
{
let mut test_result = ContractTestResult::new("Estimator".to_string());
self.test_config_immutability(estimator, &mut test_result)?;
self.test_fit_consistency(estimator, &mut test_result)?;
self.test_prediction_shape_consistency(estimator, &mut test_result)?;
self.test_error_handling_contracts(estimator, &mut test_result)?;
self.test_memory_safety_contracts(estimator, &mut test_result)?;
if self.config.include_performance_tests {
self.test_performance_contracts(estimator, &mut test_result)?;
}
self.results.push(test_result);
Ok(())
}
pub fn test_transform_contract<T>(&mut self, transformer: &T) -> Result<()>
where
T: Clone + std::fmt::Debug,
T: Transform<OwnedMatrix, OwnedMatrix>,
T: Fit<OwnedMatrix, OwnedVector>,
<T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
{
let mut test_result = ContractTestResult::new("Transform".to_string());
self.test_transform_consistency(transformer, &mut test_result)?;
self.test_fit_requirement(transformer, &mut test_result)?;
self.test_shape_transformation_contract(transformer, &mut test_result)?;
self.test_inverse_transform_properties(transformer, &mut test_result)?;
self.results.push(test_result);
Ok(())
}
pub fn test_predict_proba_contract<P>(&mut self, predictor: &P) -> Result<()>
where
P: Clone + std::fmt::Debug,
P: PredictProba<OwnedMatrix, OwnedMatrix>,
{
let mut test_result = ContractTestResult::new("PredictProba".to_string());
self.test_probability_sum_constraint(predictor, &mut test_result)?;
self.test_probability_bounds(predictor, &mut test_result)?;
self.test_predict_proba_consistency(predictor, &mut test_result)?;
self.results.push(test_result);
Ok(())
}
pub fn generate_report(&self) -> String {
let mut report = String::new();
report.push_str("# Contract Testing Report\n\n");
report.push_str(&format!("Total traits tested: {}\n", self.results.len()));
let passed_tests: usize = self
.results
.iter()
.map(|r| r.test_cases.iter().filter(|tc| tc.passed).count())
.sum();
let total_tests: usize = self.results.iter().map(|r| r.test_cases.len()).sum();
report.push_str(&format!(
"Test cases passed: {passed_tests}/{total_tests}\n"
));
report.push_str(&format!(
"Success rate: {:.2}%\n\n",
(passed_tests as f64 / total_tests as f64) * 100.0
));
for result in &self.results {
report.push_str(&format!("## {} Contract\n\n", result.trait_name));
for test_case in &result.test_cases {
let status = if test_case.passed { "✓" } else { "✗" };
report.push_str(&format!("- {} {}\n", status, test_case.test_name));
if !test_case.passed {
if let Some(ref error) = test_case.error_message {
report.push_str(&format!(" Error: {error}\n"));
}
}
if let Some(duration) = test_case.execution_time {
report.push_str(&format!(
" Execution time: {:.2}ms\n",
duration.as_millis()
));
}
}
report.push('\n');
}
report.push_str("## Property Test Statistics\n\n");
for result in &self.results {
if let Some(ref stats) = result.property_test_stats {
report.push_str(&format!(
"- {}: {} cases generated, {} edge cases found\n",
result.trait_name, stats.cases_generated, stats.edge_cases_found
));
}
}
report
}
pub fn get_summary(&self) -> ContractTestSummary {
let total_traits = self.results.len();
let total_tests: usize = self.results.iter().map(|r| r.test_cases.len()).sum();
let passed_tests: usize = self
.results
.iter()
.map(|r| r.test_cases.iter().filter(|tc| tc.passed).count())
.sum();
let total_duration: Duration = self
.results
.iter()
.flat_map(|r| &r.test_cases)
.filter_map(|tc| tc.execution_time)
.sum();
ContractTestSummary {
total_traits,
total_tests,
passed_tests,
failed_tests: total_tests - passed_tests,
success_rate: (passed_tests as f64 / total_tests as f64) * 100.0,
total_execution_time: total_duration,
}
}
fn test_config_immutability<E>(
&self,
estimator: &E,
result: &mut ContractTestResult,
) -> Result<()>
where
E: Estimator + Clone,
{
let start_time = Instant::now();
let passed = true;
let error_message = None;
let _config1 = estimator.config();
let _config2 = estimator.config();
result.test_cases.push(TestCase {
test_name: "Configuration immutability".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_fit_consistency<E>(&self, estimator: &E, result: &mut ContractTestResult) -> Result<()>
where
E: Estimator + Clone,
E: Fit<OwnedMatrix, OwnedVector>,
<E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x = Array2::from_shape_fn((20, 5), |(i, j)| (i + j) as f64);
let y = Array1::from_shape_fn(20, |i| (i % 3) as f64);
let fitted1 = estimator.clone().fit(&x, &y)?;
let fitted2 = estimator.clone().fit(&x, &y)?;
let predictions1 = fitted1.predict(&x)?;
let predictions2 = fitted2.predict(&x)?;
for (p1, p2) in predictions1.iter().zip(predictions2.iter()) {
if (p1 - p2).abs() > self.config.numerical_tolerance {
passed = false;
error_message = Some(format!("Inconsistent predictions: {p1} vs {p2}"));
break;
}
}
result.test_cases.push(TestCase {
test_name: "Fit consistency".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_prediction_shape_consistency<E>(
&self,
estimator: &E,
result: &mut ContractTestResult,
) -> Result<()>
where
E: Estimator + Clone,
E: Fit<OwnedMatrix, OwnedVector>,
<E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let sizes = vec![(10, 3), (50, 3), (100, 3)];
for (n_samples, n_features) in sizes {
let x_train = Array2::zeros((n_samples, n_features));
let y_train = Array1::zeros(n_samples);
let x_test = Array2::zeros((n_samples * 2, n_features));
let fit_result = estimator.clone().fit(&x_train, &y_train);
match fit_result {
Ok(fitted) => {
let predict_result = fitted.predict(&x_test);
match predict_result {
Ok(predictions) => {
if predictions.len() != x_test.nrows() {
passed = false;
error_message = Some(format!(
"Prediction shape mismatch: expected {}, got {}",
x_test.nrows(),
predictions.len()
));
break;
}
}
Err(e) => {
passed = false;
error_message = Some(format!("Prediction failed: {e}"));
break;
}
}
}
Err(e) => {
passed = false;
error_message = Some(format!("Fit failed: {e}"));
break;
}
};
}
result.test_cases.push(TestCase {
test_name: "Prediction shape consistency".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_error_handling_contracts<E>(
&self,
estimator: &E,
result: &mut ContractTestResult,
) -> Result<()>
where
E: Estimator + Clone,
E: Fit<OwnedMatrix, OwnedVector>,
<E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x_mismatch = Array2::zeros((10, 5));
let y_mismatch = Array1::zeros(15);
if estimator.clone().fit(&x_mismatch, &y_mismatch).is_ok() {
passed = false;
error_message = Some("Should fail with mismatched dimensions".to_string());
}
let x_empty = Array2::zeros((0, 5));
let y_empty = Array1::zeros(0);
let _ = estimator.clone().fit(&x_empty, &y_empty);
result.test_cases.push(TestCase {
test_name: "Error handling contracts".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_memory_safety_contracts<E>(
&self,
_estimator: &E,
result: &mut ContractTestResult,
) -> Result<()>
where
E: Estimator + Clone,
{
let start_time = Instant::now();
let passed = true;
result.test_cases.push(TestCase {
test_name: "Memory safety contracts".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message: None,
});
Ok(())
}
fn test_performance_contracts<E>(
&self,
estimator: &E,
result: &mut ContractTestResult,
) -> Result<()>
where
E: Estimator + Clone,
E: Fit<OwnedMatrix, OwnedVector>,
<E as Fit<OwnedMatrix, OwnedVector>>::Fitted: Predict<OwnedMatrix, OwnedVector>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let sizes = vec![100, 500, 1000];
let mut fit_times = Vec::new();
let mut predict_times = Vec::new();
for size in sizes {
let x = Array2::zeros((size, 10));
let y = Array1::zeros(size);
let fit_start = Instant::now();
let fitted = estimator.clone().fit(&x, &y)?;
let fit_time = fit_start.elapsed();
fit_times.push(fit_time);
let predict_start = Instant::now();
let _ = fitted.predict(&x)?;
let predict_time = predict_start.elapsed();
predict_times.push(predict_time);
}
if let (Some(&first_fit), Some(&last_fit)) = (fit_times.first(), fit_times.last()) {
let scaling_factor = last_fit.as_millis() as f64 / first_fit.as_millis().max(1) as f64;
if scaling_factor > 100.0 {
passed = false;
error_message = Some(format!(
"Poor performance scaling: {scaling_factor:.2}x slower for larger data"
));
}
}
result.test_cases.push(TestCase {
test_name: "Performance contracts".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_transform_consistency<T>(
&self,
transformer: &T,
result: &mut ContractTestResult,
) -> Result<()>
where
T: Clone,
T: Transform<OwnedMatrix, OwnedMatrix>,
T: Fit<OwnedMatrix, OwnedVector>,
<T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x = Array2::from_shape_fn((20, 5), |(i, j)| (i + j) as f64);
let y = Array1::zeros(20);
let fitted = transformer.clone().fit(&x, &y)?;
let transform1 = fitted.transform(&x)?;
let transform2 = fitted.transform(&x)?;
if transform1.shape() != transform2.shape() {
passed = false;
error_message = Some("Transform output shape inconsistent".to_string());
} else {
for (t1, t2) in transform1.iter().zip(transform2.iter()) {
if (t1 - t2).abs() > self.config.numerical_tolerance {
passed = false;
error_message = Some("Transform output values inconsistent".to_string());
break;
}
}
}
result.test_cases.push(TestCase {
test_name: "Transform consistency".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_fit_requirement<T>(
&self,
transformer: &T,
result: &mut ContractTestResult,
) -> Result<()>
where
T: Clone,
T: Transform<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let passed = true;
let error_message = None;
let x = Array2::zeros((10, 5));
match transformer.transform(&x) {
Ok(_) => {
}
Err(_) => {
}
}
result.test_cases.push(TestCase {
test_name: "Fit requirement".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_shape_transformation_contract<T>(
&self,
transformer: &T,
result: &mut ContractTestResult,
) -> Result<()>
where
T: Clone,
T: Transform<OwnedMatrix, OwnedMatrix>,
T: Fit<OwnedMatrix, OwnedVector>,
<T as Fit<OwnedMatrix, OwnedVector>>::Fitted: Transform<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x = Array2::from_shape_fn((25, 8), |(i, j)| (i + j) as f64);
let y = Array1::zeros(25);
let fitted = transformer.clone().fit(&x, &y)?;
let transformed = fitted.transform(&x)?;
if transformed.nrows() != x.nrows() {
passed = false;
error_message = Some(format!(
"Sample count mismatch: expected {}, got {}",
x.nrows(),
transformed.nrows()
));
}
result.test_cases.push(TestCase {
test_name: "Shape transformation contract".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_inverse_transform_properties<T>(
&self,
_transformer: &T,
result: &mut ContractTestResult,
) -> Result<()>
where
T: Clone,
T: Transform<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let passed = true;
result.test_cases.push(TestCase {
test_name: "Inverse transform properties".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message: None,
});
Ok(())
}
fn test_probability_sum_constraint<P>(
&self,
predictor: &P,
result: &mut ContractTestResult,
) -> Result<()>
where
P: PredictProba<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x = Array2::from_shape_fn((10, 5), |(i, j)| (i + j) as f64);
let probabilities = predictor.predict_proba(&x)?;
for (i, row) in probabilities.rows().into_iter().enumerate() {
let sum: f64 = row.sum();
if (sum - 1.0).abs() > self.config.numerical_tolerance {
passed = false;
error_message = Some(format!(
"Probability sum violation at sample {i}: sum = {sum}"
));
break;
}
}
result.test_cases.push(TestCase {
test_name: "Probability sum constraint".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_probability_bounds<P>(
&self,
predictor: &P,
result: &mut ContractTestResult,
) -> Result<()>
where
P: PredictProba<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let mut passed = true;
let mut error_message = None;
let x = Array2::from_shape_fn((10, 5), |(i, j)| (i + j) as f64);
let probabilities = predictor.predict_proba(&x)?;
for (i, prob) in probabilities.iter().enumerate() {
if *prob < 0.0 || *prob > 1.0 {
passed = false;
error_message = Some(format!(
"Probability out of bounds at index {i}: probability = {prob}"
));
break;
}
}
result.test_cases.push(TestCase {
test_name: "Probability bounds".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message,
});
Ok(())
}
fn test_predict_proba_consistency<P>(
&self,
_predictor: &P,
result: &mut ContractTestResult,
) -> Result<()>
where
P: PredictProba<OwnedMatrix, OwnedMatrix>,
{
let start_time = Instant::now();
let passed = true;
result.test_cases.push(TestCase {
test_name: "Predict-proba consistency".to_string(),
passed,
execution_time: Some(start_time.elapsed()),
error_message: None,
});
Ok(())
}
}
impl Default for ContractTester {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractTestResult {
pub trait_name: String,
pub test_cases: Vec<TestCase>,
pub property_test_stats: Option<PropertyTestStats>,
}
impl ContractTestResult {
fn new(trait_name: String) -> Self {
Self {
trait_name,
test_cases: Vec::new(),
property_test_stats: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestCase {
pub test_name: String,
pub passed: bool,
pub execution_time: Option<Duration>,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertyTestStats {
pub cases_generated: usize,
pub edge_cases_found: usize,
pub shrinking_attempts: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractTestSummary {
pub total_traits: usize,
pub total_tests: usize,
pub passed_tests: usize,
pub failed_tests: usize,
pub success_rate: f64,
pub total_execution_time: Duration,
}
impl fmt::Display for ContractTestSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Contract Test Summary: {}/{} tests passed ({:.1}%) across {} traits in {:.2}ms",
self.passed_tests,
self.total_tests,
self.success_rate,
self.total_traits,
self.total_execution_time.as_millis()
)
}
}
pub struct TraitLaws;
impl TraitLaws {
pub fn test_functor_laws<T>(_transformer: &T) -> Result<bool>
where
T: Clone,
T: Transform<OwnedMatrix, OwnedMatrix>,
for<'a> T: Fit<ArrayView2<'a, f64>, ArrayView1<'a, f64>>,
{
Ok(true)
}
pub fn test_monad_laws<E>(_estimator: &E) -> Result<bool>
where
E: Estimator,
{
Ok(true)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use crate::mock_objects::{MockBehavior, MockEstimator, MockTransformer};
#[test]
fn test_contract_tester_creation() {
let tester = ContractTester::new();
assert_eq!(tester.config.property_test_cases, 100);
assert!(tester.results.is_empty());
}
#[test]
fn test_estimator_contract_basic() {
let mut tester = ContractTester::new();
let estimator = MockEstimator::builder()
.with_behavior(MockBehavior::ConstantPrediction(1.0))
.build();
let result = tester.test_estimator_contract(&estimator);
assert!(result.is_ok());
assert_eq!(tester.results.len(), 1);
}
#[test]
fn test_contract_test_summary() {
let mut tester = ContractTester::new();
let estimator = MockEstimator::new();
let _ = tester.test_estimator_contract(&estimator);
let summary = tester.get_summary();
assert_eq!(summary.total_traits, 1);
assert!(summary.total_tests > 0);
}
#[test]
fn test_contract_test_report() {
let mut tester = ContractTester::new();
let estimator = MockEstimator::new();
let _ = tester.test_estimator_contract(&estimator);
let report = tester.generate_report();
assert!(report.contains("Contract Testing Report"));
assert!(report.contains("Estimator Contract"));
}
#[test]
fn test_transformer_contract() {
let mut tester = ContractTester::new();
let transformer = MockTransformer::new(crate::mock_objects::MockTransformType::Identity);
let result = tester.test_transform_contract(&transformer);
assert!(result.is_ok());
}
}