use super::{
ApplicationError, ApplicationResult, ConstraintSpec, DensitySpec, Duration, GenerationStrategy,
HashMap, Instant, InvariantScope, ProblemSpecification, ProblemType, PropertyTestResult,
PropertyType, PropertyValue, TestExecutionResult,
};
use scirs2_core::random::{thread_rng, Rng};
#[derive(Debug)]
pub struct PropertyBasedTester {
pub properties: Vec<PropertyDefinition>,
pub generators: Vec<TestCaseGenerator>,
pub shrinking_strategies: Vec<ShrinkingStrategy>,
pub execution_stats: PropertyTestStats,
}
#[derive(Debug)]
pub struct PropertyDefinition {
pub id: String,
pub description: String,
pub property_type: PropertyType,
pub preconditions: Vec<Precondition>,
pub postconditions: Vec<Postcondition>,
pub invariants: Vec<Invariant>,
}
#[derive(Debug, Clone)]
pub struct Precondition {
pub id: String,
pub expression: String,
pub parameters: HashMap<String, f64>,
}
#[derive(Debug, Clone)]
pub struct Postcondition {
pub id: String,
pub expression: String,
pub expected_result: PropertyValue,
pub tolerance: f64,
}
#[derive(Debug, Clone)]
pub struct Invariant {
pub id: String,
pub expression: String,
pub scope: InvariantScope,
}
#[derive(Debug)]
pub struct TestCaseGenerator {
pub id: String,
pub strategy: GenerationStrategy,
pub size_bounds: (usize, usize),
pub parameters: HashMap<String, f64>,
}
#[derive(Debug)]
pub struct ShrinkingStrategy {
pub id: String,
pub algorithm: ShrinkingAlgorithm,
pub max_attempts: usize,
pub parameters: HashMap<String, f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShrinkingAlgorithm {
Linear,
BinarySearch,
DeltaDebugging,
Custom(String),
}
#[derive(Debug, Default)]
pub struct PropertyTestStats {
pub cases_generated: usize,
pub cases_passed: usize,
pub cases_failed: usize,
pub shrinking_attempts: usize,
pub execution_time: Duration,
}
impl PropertyBasedTester {
#[must_use]
pub fn new() -> Self {
Self {
properties: Self::create_default_properties(),
generators: Self::create_default_generators(),
shrinking_strategies: Self::create_default_shrinking_strategies(),
execution_stats: PropertyTestStats::default(),
}
}
fn create_default_properties() -> Vec<PropertyDefinition> {
vec![
PropertyDefinition {
id: "solution_feasibility".to_string(),
description: "All solutions must be feasible".to_string(),
property_type: PropertyType::Correctness,
preconditions: vec![Precondition {
id: "valid_problem".to_string(),
expression: "problem.is_valid()".to_string(),
parameters: HashMap::new(),
}],
postconditions: vec![Postcondition {
id: "solution_valid".to_string(),
expression: "solution.is_feasible()".to_string(),
expected_result: PropertyValue::Boolean(true),
tolerance: 0.0,
}],
invariants: vec![Invariant {
id: "energy_conservation".to_string(),
expression: "energy_is_conserved".to_string(),
scope: InvariantScope::Global,
}],
},
PropertyDefinition {
id: "optimization_monotonicity".to_string(),
description: "Optimization should improve or maintain solution quality".to_string(),
property_type: PropertyType::Performance,
preconditions: vec![Precondition {
id: "initial_solution".to_string(),
expression: "has_initial_solution".to_string(),
parameters: HashMap::new(),
}],
postconditions: vec![Postcondition {
id: "quality_improvement".to_string(),
expression: "final_quality >= initial_quality".to_string(),
expected_result: PropertyValue::Boolean(true),
tolerance: 0.001,
}],
invariants: vec![Invariant {
id: "quality_monotonic".to_string(),
expression: "quality_non_decreasing".to_string(),
scope: InvariantScope::Temporal,
}],
},
PropertyDefinition {
id: "deterministic_behavior".to_string(),
description: "Same input should produce same output with fixed seed".to_string(),
property_type: PropertyType::Consistency,
preconditions: vec![Precondition {
id: "fixed_seed".to_string(),
expression: "seed.is_fixed()".to_string(),
parameters: HashMap::new(),
}],
postconditions: vec![Postcondition {
id: "reproducible_result".to_string(),
expression: "result1 == result2".to_string(),
expected_result: PropertyValue::Boolean(true),
tolerance: 0.0,
}],
invariants: Vec::new(),
},
PropertyDefinition {
id: "resource_bounds".to_string(),
description: "Resource usage should remain within bounds".to_string(),
property_type: PropertyType::Safety,
preconditions: Vec::new(),
postconditions: vec![
Postcondition {
id: "memory_bounded".to_string(),
expression: "memory_usage <= max_memory".to_string(),
expected_result: PropertyValue::Boolean(true),
tolerance: 0.0,
},
Postcondition {
id: "time_bounded".to_string(),
expression: "execution_time <= max_time".to_string(),
expected_result: PropertyValue::Boolean(true),
tolerance: 0.0,
},
],
invariants: vec![Invariant {
id: "resource_limits".to_string(),
expression: "within_resource_limits".to_string(),
scope: InvariantScope::Global,
}],
},
]
}
fn create_default_generators() -> Vec<TestCaseGenerator> {
vec![
TestCaseGenerator {
id: "random_ising_generator".to_string(),
strategy: GenerationStrategy::Random,
size_bounds: (5, 100),
parameters: {
let mut params = HashMap::new();
params.insert("density".to_string(), 0.3);
params.insert("bias_range".to_string(), 2.0);
params.insert("coupling_range".to_string(), 1.0);
params
},
},
TestCaseGenerator {
id: "boundary_value_generator".to_string(),
strategy: GenerationStrategy::BoundaryValue,
size_bounds: (1, 1000),
parameters: {
let mut params = HashMap::new();
params.insert("boundary_offset".to_string(), 1.0);
params
},
},
TestCaseGenerator {
id: "equivalence_class_generator".to_string(),
strategy: GenerationStrategy::EquivalenceClass,
size_bounds: (10, 50),
parameters: {
let mut params = HashMap::new();
params.insert("num_classes".to_string(), 5.0);
params
},
},
]
}
fn create_default_shrinking_strategies() -> Vec<ShrinkingStrategy> {
vec![
ShrinkingStrategy {
id: "linear_shrinking".to_string(),
algorithm: ShrinkingAlgorithm::Linear,
max_attempts: 100,
parameters: {
let mut params = HashMap::new();
params.insert("shrink_factor".to_string(), 0.5);
params
},
},
ShrinkingStrategy {
id: "binary_search_shrinking".to_string(),
algorithm: ShrinkingAlgorithm::BinarySearch,
max_attempts: 50,
parameters: HashMap::new(),
},
ShrinkingStrategy {
id: "delta_debugging".to_string(),
algorithm: ShrinkingAlgorithm::DeltaDebugging,
max_attempts: 200,
parameters: {
let mut params = HashMap::new();
params.insert("granularity".to_string(), 2.0);
params
},
},
]
}
pub fn run_property_tests(
&mut self,
property_id: &str,
num_cases: usize,
) -> ApplicationResult<PropertyTestResult> {
let property = self
.properties
.iter()
.find(|p| p.id == property_id)
.ok_or_else(|| {
ApplicationError::ConfigurationError(format!("Property not found: {property_id}"))
})?
.clone();
println!("Running property-based tests for: {}", property.id);
let start_time = Instant::now();
let mut cases_tested = 0;
let mut cases_passed = 0;
let mut counterexamples = Vec::new();
for _ in 0..num_cases {
let test_case = self.generate_test_case(&property)?;
cases_tested += 1;
let result = self.test_property(&property, &test_case)?;
if result.passed {
cases_passed += 1;
} else {
let shrunk_case = {
self.execution_stats.shrinking_attempts += 1;
self.shrink_counterexample_internal(&property, &test_case)?
};
counterexamples.push(format!("Case {cases_tested}: {shrunk_case:?}"));
if counterexamples.len() >= 3 {
break;
}
}
}
let execution_time = start_time.elapsed();
let confidence = if cases_tested > 0 {
cases_passed as f64 / cases_tested as f64
} else {
0.0
};
self.execution_stats.cases_generated += cases_tested;
self.execution_stats.cases_passed += cases_passed;
self.execution_stats.cases_failed += cases_tested - cases_passed;
self.execution_stats.execution_time += execution_time;
println!("Property test completed: {cases_passed}/{cases_tested} passed");
Ok(PropertyTestResult {
property_id: property.id.clone(),
cases_tested,
cases_passed,
counterexamples,
confidence,
execution_time,
})
}
fn generate_test_case(
&self,
property: &PropertyDefinition,
) -> ApplicationResult<PropertyTestCase> {
let generator = self
.generators
.iter()
.find(|g| self.is_generator_suitable(g, property))
.ok_or_else(|| {
ApplicationError::ConfigurationError(
"No suitable generator found for property".to_string(),
)
})?;
self.generate_with_strategy(generator, property)
}
const fn is_generator_suitable(
&self,
_generator: &TestCaseGenerator,
_property: &PropertyDefinition,
) -> bool {
true
}
fn generate_with_strategy(
&self,
generator: &TestCaseGenerator,
_property: &PropertyDefinition,
) -> ApplicationResult<PropertyTestCase> {
match generator.strategy {
GenerationStrategy::Random => self.generate_random_case(generator),
GenerationStrategy::BoundaryValue => self.generate_boundary_case(generator),
GenerationStrategy::EquivalenceClass => self.generate_equivalence_case(generator),
_ => self.generate_random_case(generator), }
}
fn generate_random_case(
&self,
generator: &TestCaseGenerator,
) -> ApplicationResult<PropertyTestCase> {
let mut rng = thread_rng();
let size = rng.random_range(generator.size_bounds.0..=generator.size_bounds.1);
let density = generator.parameters.get("density").unwrap_or(&0.3);
let bias_range = generator.parameters.get("bias_range").unwrap_or(&1.0);
Ok(PropertyTestCase {
id: format!("random_case_{}", thread_rng().random::<u32>()),
problem_spec: ProblemSpecification {
problem_type: ProblemType::RandomIsing,
size_range: (size, size),
density: DensitySpec {
edge_density: (*density, *density),
constraint_density: None,
bias_sparsity: None,
},
constraints: ConstraintSpec {
num_constraints: None,
constraint_types: Vec::new(),
strength_range: (0.1, *bias_range),
},
seed: Some(rng.random()),
},
input_parameters: {
let mut params = HashMap::new();
params.insert("size".to_string(), PropertyValue::Numeric(size as f64));
params.insert("density".to_string(), PropertyValue::Numeric(*density));
params
},
expected_properties: Vec::new(),
})
}
fn generate_boundary_case(
&self,
generator: &TestCaseGenerator,
) -> ApplicationResult<PropertyTestCase> {
let boundary_sizes = vec![
generator.size_bounds.0,
generator.size_bounds.0 + 1,
generator.size_bounds.1 - 1,
generator.size_bounds.1,
];
let mut rng = thread_rng();
let size = boundary_sizes[rng.random_range(0..boundary_sizes.len())];
Ok(PropertyTestCase {
id: format!("boundary_case_{size}"),
problem_spec: ProblemSpecification {
problem_type: ProblemType::RandomIsing,
size_range: (size, size),
density: DensitySpec {
edge_density: (0.1, 0.1),
constraint_density: None,
bias_sparsity: None,
},
constraints: ConstraintSpec {
num_constraints: None,
constraint_types: Vec::new(),
strength_range: (0.1, 1.0),
},
seed: Some(42),
},
input_parameters: {
let mut params = HashMap::new();
params.insert("size".to_string(), PropertyValue::Numeric(size as f64));
params.insert(
"boundary_type".to_string(),
PropertyValue::String("size_boundary".to_string()),
);
params
},
expected_properties: Vec::new(),
})
}
fn generate_equivalence_case(
&self,
generator: &TestCaseGenerator,
) -> ApplicationResult<PropertyTestCase> {
let num_classes = *generator.parameters.get("num_classes").unwrap_or(&5.0) as usize;
let mut rng = thread_rng();
let class_id = rng.random_range(0..num_classes);
let (problem_type, density) = match class_id {
0 => (ProblemType::RandomIsing, 0.1), 1 => (ProblemType::RandomIsing, 0.5), 2 => (ProblemType::MaxCut, 0.3), 3 => (ProblemType::VertexCover, 0.2), _ => (ProblemType::RandomIsing, 0.3), };
let mut rng = thread_rng();
let size = rng.random_range(generator.size_bounds.0..=generator.size_bounds.1);
Ok(PropertyTestCase {
id: format!("equiv_case_{class_id}_{size}"),
problem_spec: ProblemSpecification {
problem_type,
size_range: (size, size),
density: DensitySpec {
edge_density: (density, density),
constraint_density: None,
bias_sparsity: None,
},
constraints: ConstraintSpec {
num_constraints: None,
constraint_types: Vec::new(),
strength_range: (0.1, 1.0),
},
seed: Some(42 + class_id as u64),
},
input_parameters: {
let mut params = HashMap::new();
params.insert(
"equivalence_class".to_string(),
PropertyValue::Numeric(class_id as f64),
);
params.insert("size".to_string(), PropertyValue::Numeric(size as f64));
params
},
expected_properties: Vec::new(),
})
}
fn test_property(
&self,
property: &PropertyDefinition,
test_case: &PropertyTestCase,
) -> ApplicationResult<PropertyTestCaseResult> {
for precondition in &property.preconditions {
if !self.evaluate_precondition(precondition, test_case)? {
return Ok(PropertyTestCaseResult {
test_case_id: test_case.id.clone(),
passed: false,
failure_reason: Some(format!("Precondition failed: {}", precondition.id)),
execution_time: Duration::from_millis(1),
property_values: HashMap::new(),
});
}
}
let start_time = Instant::now();
let execution_result = self.simulate_test_execution(test_case)?;
let execution_time = start_time.elapsed();
let mut all_passed = true;
let mut failure_reason = None;
let mut property_values = HashMap::new();
for postcondition in &property.postconditions {
let result = self.evaluate_postcondition(postcondition, &execution_result)?;
property_values.insert(postcondition.id.clone(), result.actual_value.clone());
if !result.passed {
all_passed = false;
failure_reason = Some(format!(
"Postcondition failed: {} (expected: {:?}, actual: {:?})",
postcondition.id, postcondition.expected_result, result.actual_value
));
break;
}
}
if all_passed {
for invariant in &property.invariants {
if !self.evaluate_invariant(invariant, &execution_result)? {
all_passed = false;
failure_reason = Some(format!("Invariant violated: {}", invariant.id));
break;
}
}
}
Ok(PropertyTestCaseResult {
test_case_id: test_case.id.clone(),
passed: all_passed,
failure_reason,
execution_time,
property_values,
})
}
const fn evaluate_precondition(
&self,
_precondition: &Precondition,
_test_case: &PropertyTestCase,
) -> ApplicationResult<bool> {
Ok(true)
}
fn simulate_test_execution(
&self,
test_case: &PropertyTestCase,
) -> ApplicationResult<TestExecutionResult> {
let size = match test_case.input_parameters.get("size") {
Some(PropertyValue::Numeric(s)) => *s as usize,
_ => 10,
};
let quality = thread_rng().random::<f64>().mul_add(0.2, 0.8);
let execution_time = Duration::from_millis((size as u64 * 10).min(1000));
Ok(TestExecutionResult {
solution_quality: quality,
execution_time,
final_energy: -quality * size as f64,
best_solution: vec![1; size],
convergence_achieved: quality > 0.9,
memory_used: size * 8,
})
}
fn evaluate_postcondition(
&self,
postcondition: &Postcondition,
execution_result: &TestExecutionResult,
) -> ApplicationResult<PostconditionResult> {
let actual_value = match postcondition.id.as_str() {
"solution_valid" => PropertyValue::Boolean(execution_result.convergence_achieved),
"quality_improvement" => PropertyValue::Numeric(execution_result.solution_quality),
"reproducible_result" => PropertyValue::Boolean(true), "memory_bounded" => PropertyValue::Boolean(execution_result.memory_used < 1_000_000),
"time_bounded" => {
PropertyValue::Boolean(execution_result.execution_time < Duration::from_secs(60))
}
_ => PropertyValue::Boolean(true),
};
let passed = match (&postcondition.expected_result, &actual_value) {
(PropertyValue::Boolean(expected), PropertyValue::Boolean(actual)) => {
expected == actual
}
(PropertyValue::Numeric(expected), PropertyValue::Numeric(actual)) => {
(expected - actual).abs() <= postcondition.tolerance
}
_ => false,
};
Ok(PostconditionResult {
postcondition_id: postcondition.id.clone(),
passed,
expected_value: postcondition.expected_result.clone(),
actual_value,
deviation: 0.0, })
}
const fn evaluate_invariant(
&self,
_invariant: &Invariant,
_execution_result: &TestExecutionResult,
) -> ApplicationResult<bool> {
Ok(true)
}
fn shrink_counterexample_internal(
&self,
_property: &PropertyDefinition,
test_case: &PropertyTestCase,
) -> ApplicationResult<PropertyTestCase> {
let current_size = match test_case.input_parameters.get("size") {
Some(PropertyValue::Numeric(s)) => (*s as usize).max(1),
_ => 1,
};
let shrunk_size = (current_size / 2).max(1);
let mut shrunk_case = test_case.clone();
shrunk_case.id = format!("{}_shrunk", test_case.id);
shrunk_case.problem_spec.size_range = (shrunk_size, shrunk_size);
shrunk_case.input_parameters.insert(
"size".to_string(),
PropertyValue::Numeric(shrunk_size as f64),
);
Ok(shrunk_case)
}
pub fn add_property(&mut self, property: PropertyDefinition) {
self.properties.push(property);
}
#[must_use]
pub fn get_property(&self, property_id: &str) -> Option<&PropertyDefinition> {
self.properties.iter().find(|p| p.id == property_id)
}
pub fn add_generator(&mut self, generator: TestCaseGenerator) {
self.generators.push(generator);
}
#[must_use]
pub const fn get_stats(&self) -> &PropertyTestStats {
&self.execution_stats
}
}
#[derive(Debug, Clone)]
pub struct PropertyTestCase {
pub id: String,
pub problem_spec: ProblemSpecification,
pub input_parameters: HashMap<String, PropertyValue>,
pub expected_properties: Vec<String>,
}
#[derive(Debug)]
pub struct PropertyTestCaseResult {
pub test_case_id: String,
pub passed: bool,
pub failure_reason: Option<String>,
pub execution_time: Duration,
pub property_values: HashMap<String, PropertyValue>,
}
#[derive(Debug)]
pub struct PostconditionResult {
pub postcondition_id: String,
pub passed: bool,
pub expected_value: PropertyValue,
pub actual_value: PropertyValue,
pub deviation: f64,
}