use crate::error::{CoreError, CoreResult};
use crate::testing::{TestConfig, TestResult};
use std::fmt::Debug;
use std::time::{Duration, Instant};
#[cfg(feature = "random")]
use rand::rngs::StdRng;
#[cfg(feature = "random")]
use rand::{Rng, SeedableRng};
pub trait MathematicalProperty<T> {
fn name(&self) -> &str;
fn test(&self, inputs: &[T]) -> CoreResult<bool>;
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<T>) -> CoreResult<Vec<T>>;
}
pub trait PropertyGenerator<T> {
fn generate(&mut self) -> T;
fn generate_range(&mut self, min: T, max: T) -> T;
fn generate_related(&mut self, count: usize) -> Vec<T>;
}
#[derive(Debug, Clone)]
pub struct PropertyTestConfig {
pub test_cases: usize,
pub max_shrink_attempts: usize,
pub seed: Option<u64>,
pub float_tolerance: f64,
pub verbose: bool,
}
impl Default for PropertyTestConfig {
fn default() -> Self {
Self {
test_cases: 1000,
max_shrink_attempts: 100,
seed: None,
float_tolerance: 1e-10,
verbose: false,
}
}
}
impl PropertyTestConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_test_cases(mut self, cases: usize) -> Self {
self.test_cases = cases;
self
}
pub fn with_max_shrink_attempts(mut self, attempts: usize) -> Self {
self.max_shrink_attempts = attempts;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_float_tolerance(mut self, tolerance: f64) -> Self {
self.float_tolerance = tolerance;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
}
#[derive(Debug, Clone)]
pub struct PropertyTestResult {
pub property_name: String,
pub cases_executed: usize,
pub cases_passed: usize,
pub duration: Duration,
pub failures: Vec<PropertyFailure>,
pub property_satisfied: bool,
}
#[derive(Debug, Clone)]
pub struct PropertyFailure {
pub case_number: usize,
pub inputs: Vec<String>,
pub expected: String,
pub actual: String,
pub error: Option<String>,
}
pub struct PropertyTestEngine {
config: PropertyTestConfig,
#[cfg(feature = "random")]
#[allow(dead_code)]
rng: StdRng,
}
impl PropertyTestEngine {
pub fn new(config: PropertyTestConfig) -> Self {
#[cfg(feature = "random")]
let rng = if let Some(seed) = config.seed {
StdRng::seed_from_u64(seed)
} else {
StdRng::seed_from_u64(Default::default())
};
Self {
config,
#[cfg(feature = "random")]
rng,
}
}
pub fn test_property<T>(
&mut self,
property: &dyn MathematicalProperty<T>,
generator: &mut dyn PropertyGenerator<T>,
) -> CoreResult<PropertyTestResult>
where
T: Debug + Clone,
{
let start_time = Instant::now();
let mut failures = Vec::new();
let mut cases_passed = 0;
if self.config.verbose {
println!("Testing property: {}", property.name());
}
for case_num in 0..self.config.test_cases {
let inputs = property.generate_inputs(generator)?;
match property.test(&inputs) {
Ok(true) => {
cases_passed += 1;
}
Ok(false) => {
failures.push(PropertyFailure {
case_number: case_num,
inputs: inputs.iter().map(|i| format!("{i:?}")).collect(),
expected: "Property should hold".to_string(),
actual: "Property violated".to_string(),
error: None,
});
}
Err(error) => {
failures.push(PropertyFailure {
case_number: case_num,
inputs: inputs.iter().map(|i| format!("{i:?}")).collect(),
expected: "Property should hold".to_string(),
actual: "Error during testing".to_string(),
error: Some(format!("{error:?}")),
});
}
}
}
let duration = start_time.elapsed();
let property_satisfied = failures.is_empty();
if self.config.verbose {
println!(
"Property {} completed: {}/{} cases passed",
property.name(),
cases_passed,
self.config.test_cases
);
}
Ok(PropertyTestResult {
property_name: property.name().to_string(),
cases_executed: self.config.test_cases,
cases_passed,
duration,
failures,
property_satisfied,
})
}
pub fn test_properties<T>(
&mut self,
properties: Vec<&dyn MathematicalProperty<T>>,
generator: &mut dyn PropertyGenerator<T>,
) -> CoreResult<Vec<PropertyTestResult>>
where
T: Debug + Clone,
{
let mut results = Vec::new();
for property in properties {
let result = self.test_property(property, generator)?;
results.push(result);
}
Ok(results)
}
}
pub struct FloatGenerator {
#[cfg(feature = "random")]
rng: StdRng,
min_value: f64,
max_value: f64,
}
impl FloatGenerator {
pub fn new(min_value: f64, maxvalue: f64) -> Self {
Self {
#[cfg(feature = "random")]
rng: StdRng::seed_from_u64(Default::default()),
min_value,
max_value,
}
}
#[allow(unused_variables)]
pub fn with_seed(min_value: f64, maxvalue: f64, seed: u64) -> Self {
Self {
#[cfg(feature = "random")]
rng: StdRng::seed_from_u64(seed),
min_value,
max_value,
}
}
}
#[allow(deprecated)]
impl PropertyGenerator<f64> for FloatGenerator {
fn generate(&mut self) -> f64 {
#[cfg(feature = "random")]
{
self.rng.random_range(self.min_value..=self.max_value)
}
#[cfg(not(feature = "random"))]
{
(self.min_value + self.max_value) / 2.0
}
}
fn generate_range(&mut self, min: f64, max: f64) -> f64 {
#[cfg(feature = "random")]
{
self.rng.random_range(min..=max)
}
#[cfg(not(feature = "random"))]
{
(min + max) / 2.0
}
}
fn generate_related(&mut self, count: usize) -> Vec<f64> {
(0..count).map(|_| self.generate()).collect()
}
}
pub struct AssociativityProperty<F> {
operation: F,
tolerance: f64,
}
impl<F> AssociativityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
pub fn new(operation: F, tolerance: f64) -> Self {
Self {
operation,
tolerance,
}
}
}
impl<F> MathematicalProperty<f64> for AssociativityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
fn name(&self) -> &str {
"Associativity"
}
fn test(&self, inputs: &[f64]) -> CoreResult<bool> {
if inputs.len() != 3 {
return Err(CoreError::ValidationError(crate::error::ErrorContext::new(
"Associativity test requires exactly 3 inputs",
)));
}
let a = inputs[0];
let b = inputs[1];
let c = inputs[2];
let ab = (self.operation)(a, b)?;
let ab_c = (self.operation)(ab, c)?;
let bc = (self.operation)(b, c)?;
let a_bc = (self.operation)(a, bc)?;
let diff = (ab_c - a_bc).abs();
Ok(diff <= self.tolerance)
}
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<f64>) -> CoreResult<Vec<f64>> {
Ok(generator.generate_related(3))
}
}
pub struct CommutativityProperty<F> {
operation: F,
tolerance: f64,
}
impl<F> CommutativityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
pub fn new(operation: F, tolerance: f64) -> Self {
Self {
operation,
tolerance,
}
}
}
impl<F> MathematicalProperty<f64> for CommutativityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
fn name(&self) -> &str {
"Commutativity"
}
fn test(&self, inputs: &[f64]) -> CoreResult<bool> {
if inputs.len() != 2 {
return Err(CoreError::ValidationError(crate::error::ErrorContext::new(
"Commutativity test requires exactly 2 inputs",
)));
}
let a = inputs[0];
let b = inputs[1];
let ab = (self.operation)(a, b)?;
let ba = (self.operation)(b, a)?;
let diff = (ab - ba).abs();
Ok(diff <= self.tolerance)
}
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<f64>) -> CoreResult<Vec<f64>> {
Ok(generator.generate_related(2))
}
}
pub struct IdentityProperty<F> {
operation: F,
identity_element: f64,
tolerance: f64,
}
impl<F> IdentityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
pub fn new(operation: F, identityelement: f64, tolerance: f64) -> Self {
Self {
operation,
identity_element,
tolerance,
}
}
}
impl<F> MathematicalProperty<f64> for IdentityProperty<F>
where
F: Fn(f64, f64) -> CoreResult<f64>,
{
fn name(&self) -> &str {
"Identity"
}
fn test(&self, inputs: &[f64]) -> CoreResult<bool> {
if inputs.len() != 1 {
return Err(CoreError::ValidationError(crate::error::ErrorContext::new(
"Identity test requires exactly 1 input",
)));
}
let a = inputs[0];
let e = self.identity_element;
let ae = (self.operation)(a, e)?;
let diff1 = (ae - a).abs();
let ea = (self.operation)(e, a)?;
let diff2 = (ea - a).abs();
Ok(diff1 <= self.tolerance && diff2 <= self.tolerance)
}
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<f64>) -> CoreResult<Vec<f64>> {
Ok(vec![generator.generate()])
}
}
pub struct IdempotencyProperty<F> {
function: F,
tolerance: f64,
}
impl<F> IdempotencyProperty<F>
where
F: Fn(f64) -> CoreResult<f64>,
{
pub fn new(function: F, tolerance: f64) -> Self {
Self {
function,
tolerance,
}
}
}
impl<F> MathematicalProperty<f64> for IdempotencyProperty<F>
where
F: Fn(f64) -> CoreResult<f64>,
{
fn name(&self) -> &str {
"Idempotency"
}
fn test(&self, inputs: &[f64]) -> CoreResult<bool> {
if inputs.len() != 1 {
return Err(CoreError::ValidationError(crate::error::ErrorContext::new(
"Idempotency test requires exactly 1 input",
)));
}
let x = inputs[0];
let fx = (self.function)(x)?;
let ffx = (self.function)(fx)?;
let diff = (ffx - fx).abs();
Ok(diff <= self.tolerance)
}
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<f64>) -> CoreResult<Vec<f64>> {
Ok(vec![generator.generate()])
}
}
pub struct MonotonicityProperty<F> {
function: F,
increasing: bool, }
impl<F> MonotonicityProperty<F>
where
F: Fn(f64) -> CoreResult<f64>,
{
pub fn increasing(function: F) -> Self {
Self {
function,
increasing: true,
}
}
pub fn decreasing(function: F) -> Self {
Self {
function,
increasing: false,
}
}
}
impl<F> MathematicalProperty<f64> for MonotonicityProperty<F>
where
F: Fn(f64) -> CoreResult<f64>,
{
fn name(&self) -> &str {
if self.increasing {
"Monotonicity (Increasing)"
} else {
"Monotonicity (Decreasing)"
}
}
fn test(&self, inputs: &[f64]) -> CoreResult<bool> {
if inputs.len() != 2 {
return Err(CoreError::ValidationError(crate::error::ErrorContext::new(
"Monotonicity test requires exactly 2 inputs",
)));
}
let x = inputs[0];
let y = inputs[1];
if x > y {
return self.test(&[y, x]);
}
let fx = (self.function)(x)?;
let fy = (self.function)(y)?;
if self.increasing {
Ok(fx <= fy)
} else {
Ok(fx >= fy)
}
}
fn generate_inputs(&self, generator: &mut dyn PropertyGenerator<f64>) -> CoreResult<Vec<f64>> {
let mut inputs = generator.generate_related(2);
if inputs[0] > inputs[1] {
inputs.swap(0, 1);
}
Ok(inputs)
}
}
pub struct PropertyTestUtils;
impl PropertyTestUtils {
pub fn test_arithmetic_properties<F>(
operation: F,
identity: Option<f64>,
config: PropertyTestConfig,
) -> CoreResult<Vec<PropertyTestResult>>
where
F: Fn(f64, f64) -> CoreResult<f64> + Clone,
{
let mut engine = PropertyTestEngine::new(config.clone());
let mut generator = FloatGenerator::new(-100.0, 100.0);
let mut properties: Vec<Box<dyn MathematicalProperty<f64>>> = Vec::new();
properties.push(Box::new(AssociativityProperty::new(
operation.clone(),
config.float_tolerance,
)));
properties.push(Box::new(CommutativityProperty::new(
operation.clone(),
config.float_tolerance,
)));
if let Some(identity_value) = identity {
properties.push(Box::new(IdentityProperty::new(
operation,
identity_value,
config.float_tolerance,
)));
}
let property_refs: Vec<&dyn MathematicalProperty<f64>> =
properties.iter().map(|p| p.as_ref()).collect();
engine.test_properties(property_refs, &mut generator)
}
pub fn test_function_properties<F>(
function: F,
is_idempotent: bool,
is_monotonic: Option<bool>, config: PropertyTestConfig,
) -> CoreResult<Vec<PropertyTestResult>>
where
F: Fn(f64) -> CoreResult<f64> + Clone,
{
let mut engine = PropertyTestEngine::new(config.clone());
let mut generator = FloatGenerator::new(-100.0, 100.0);
let mut properties: Vec<Box<dyn MathematicalProperty<f64>>> = Vec::new();
if is_idempotent {
properties.push(Box::new(IdempotencyProperty::new(
function.clone(),
config.float_tolerance,
)));
}
if let Some(increasing) = is_monotonic {
if increasing {
properties.push(Box::new(MonotonicityProperty::increasing(function)));
} else {
properties.push(Box::new(MonotonicityProperty::decreasing(function)));
}
}
let property_refs: Vec<&dyn MathematicalProperty<f64>> =
properties.iter().map(|p| p.as_ref()).collect();
engine.test_properties(property_refs, &mut generator)
}
pub fn create_property_test_suite(name: &str, config: TestConfig) -> crate::testing::TestSuite {
let mut suite = crate::testing::TestSuite::new(name, config);
suite.add_test("addition_properties", |_runner| {
let prop_config = PropertyTestConfig::default().with_test_cases(100);
let results = Self::test_arithmetic_properties(
|a, b| Ok(a + b),
Some(0.0), prop_config,
)?;
let all_passed = results.iter().all(|r| r.property_satisfied);
if !all_passed {
let failed_properties: Vec<&str> = results
.iter()
.filter(|r| !r.property_satisfied)
.map(|r| r.property_name.as_str())
.collect();
return Ok(TestResult::failure(
Duration::from_millis(100),
100,
format!("Failed properties: {:?}", failed_properties),
));
}
Ok(TestResult::success(Duration::from_millis(100), 100))
});
suite.add_test("multiplication_properties", |_runner| {
let prop_config = PropertyTestConfig::default().with_test_cases(100);
let results = Self::test_arithmetic_properties(
|a, b| Ok(a * b),
Some(1.0), prop_config,
)?;
let all_passed = results.iter().all(|r| r.property_satisfied);
if !all_passed {
let failed_properties: Vec<&str> = results
.iter()
.filter(|r| !r.property_satisfied)
.map(|r| r.property_name.as_str())
.collect();
return Ok(TestResult::failure(
Duration::from_millis(100),
100,
format!("Failed properties: {:?}", failed_properties),
));
}
Ok(TestResult::success(Duration::from_millis(100), 100))
});
suite.add_test("square_function_properties", |_runner| {
let prop_config = PropertyTestConfig::default().with_test_cases(100);
let results = Self::test_function_properties(
|x| Ok(x * x),
false, Some(true), prop_config,
)?;
Ok(TestResult::success(Duration::from_millis(100), 100))
});
suite
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_associativity_property() {
let property = AssociativityProperty::new(|a, b| Ok(a + b), 1e-10);
let inputs = vec![1.0, 2.0, 3.0];
let result = property.test(&inputs).expect("Operation failed");
assert!(result); }
#[test]
fn test_commutativity_property() {
let property = CommutativityProperty::new(|a, b| Ok(a + b), 1e-10);
let inputs = vec![1.0, 2.0];
let result = property.test(&inputs).expect("Operation failed");
assert!(result); }
#[test]
fn test_identity_property() {
let property = IdentityProperty::new(|a, b| Ok(a + b), 0.0, 1e-10);
let inputs = vec![5.0];
let result = property.test(&inputs).expect("Operation failed");
assert!(result); }
#[test]
fn test_idempotency_property() {
let property = IdempotencyProperty::new(|x| Ok(x.abs()), 1e-10);
let inputs = vec![5.0];
let result = property.test(&inputs).expect("Operation failed");
assert!(result); }
#[test]
fn test_monotonicity_property() {
let property = MonotonicityProperty::increasing(|x| Ok(x * x));
let inputs = vec![2.0, 3.0];
let result = property.test(&inputs).expect("Operation failed");
assert!(result);
}
#[test]
fn test_float_generator() {
let mut generator = FloatGenerator::new(-10.0, 10.0);
for _ in 0..100 {
let value = generator.generate();
assert!((-10.0..=10.0).contains(&value));
}
let related = generator.generate_related(5);
assert_eq!(related.len(), 5);
}
}