use ddex_builder::{Builder, BuildRequest};
use proptest::prelude::*;
use std::collections::HashMap;
use std::time::Instant;
pub mod generators;
pub mod strategies;
pub mod validation;
#[derive(Debug, Clone)]
pub struct DeterminismTestConfig {
pub iterations: usize,
pub test_multithreading: bool,
pub test_memory_constraints: bool,
pub test_random_seeds: bool,
pub max_complexity: usize,
}
impl Default for DeterminismTestConfig {
fn default() -> Self {
Self {
iterations: 100,
test_multithreading: true,
test_memory_constraints: true,
test_random_seeds: true,
max_complexity: 1000,
}
}
}
#[derive(Debug, Clone)]
pub struct DeterminismTestResult {
pub test_name: String,
pub total_iterations: usize,
pub successful_iterations: usize,
pub failed_iterations: usize,
pub unique_outputs: usize,
pub build_times_ms: Vec<u64>,
pub output_sizes: Vec<usize>,
pub errors: Vec<String>,
}
impl DeterminismTestResult {
pub fn new(test_name: String) -> Self {
Self {
test_name,
total_iterations: 0,
successful_iterations: 0,
failed_iterations: 0,
unique_outputs: 0,
build_times_ms: Vec::new(),
output_sizes: Vec::new(),
errors: Vec::new(),
}
}
pub fn is_deterministic(&self) -> bool {
self.unique_outputs <= 1 && self.successful_iterations > 0
}
pub fn success_rate(&self) -> f64 {
if self.total_iterations == 0 {
0.0
} else {
self.successful_iterations as f64 / self.total_iterations as f64
}
}
pub fn average_build_time_ms(&self) -> f64 {
if self.build_times_ms.is_empty() {
0.0
} else {
self.build_times_ms.iter().sum::<u64>() as f64 / self.build_times_ms.len() as f64
}
}
pub fn generate_report(&self) -> String {
format!(
"Determinism Test Report: {}\n\
=============================\n\
Total iterations: {}\n\
Successful iterations: {}\n\
Failed iterations: {}\n\
Unique outputs: {}\n\
Deterministic: {}\n\
Success rate: {:.2}%\n\
Average build time: {:.2}ms\n\
Average output size: {:.0} bytes\n\
Errors: {}\n",
self.test_name,
self.total_iterations,
self.successful_iterations,
self.failed_iterations,
self.unique_outputs,
if self.is_deterministic() { "YES" } else { "NO" },
self.success_rate() * 100.0,
self.average_build_time_ms(),
if self.output_sizes.is_empty() { 0.0 } else {
self.output_sizes.iter().sum::<usize>() as f64 / self.output_sizes.len() as f64
},
self.errors.len()
)
}
}
pub struct DeterminismTestRunner {
config: DeterminismTestConfig,
}
impl DeterminismTestRunner {
pub fn new(config: DeterminismTestConfig) -> Self {
Self { config }
}
pub async fn run_all_tests(&self) -> Result<Vec<DeterminismTestResult>, Box<dyn std::error::Error>> {
let mut results = Vec::new();
results.push(self.test_basic_determinism().await?);
results.push(self.test_complex_structure_determinism().await?);
if self.config.test_multithreading {
results.push(self.test_concurrent_determinism().await?);
}
if self.config.test_memory_constraints {
results.push(self.test_memory_constrained_determinism().await?);
}
if self.config.test_random_seeds {
results.push(self.test_seeded_determinism().await?);
}
Ok(results)
}
pub async fn test_basic_determinism(&self) -> Result<DeterminismTestResult, Box<dyn std::error::Error>> {
let mut result = DeterminismTestResult::new("Basic Determinism".to_string());
let mut unique_outputs = HashMap::new();
for _ in 0..self.config.iterations {
result.total_iterations += 1;
let build_request = self.generate_simple_build_request();
let start_time = Instant::now();
match self.build_xml(&build_request) {
Ok(xml) => {
let build_time = start_time.elapsed().as_millis() as u64;
result.successful_iterations += 1;
result.build_times_ms.push(build_time);
result.output_sizes.push(xml.len());
let output_hash = self.hash_xml(&xml);
*unique_outputs.entry(output_hash).or_insert(0) += 1;
},
Err(e) => {
result.failed_iterations += 1;
result.errors.push(e.to_string());
}
}
}
result.unique_outputs = unique_outputs.len();
Ok(result)
}
pub async fn test_complex_structure_determinism(&self) -> Result<DeterminismTestResult, Box<dyn std::error::Error>> {
let mut result = DeterminismTestResult::new("Complex Structure Determinism".to_string());
let mut unique_outputs = HashMap::new();
for _ in 0..self.config.iterations {
result.total_iterations += 1;
let build_request = self.generate_complex_build_request();
let start_time = Instant::now();
match self.build_xml(&build_request) {
Ok(xml) => {
let build_time = start_time.elapsed().as_millis() as u64;
result.successful_iterations += 1;
result.build_times_ms.push(build_time);
result.output_sizes.push(xml.len());
let output_hash = self.hash_xml(&xml);
*unique_outputs.entry(output_hash).or_insert(0) += 1;
},
Err(e) => {
result.failed_iterations += 1;
result.errors.push(e.to_string());
}
}
}
result.unique_outputs = unique_outputs.len();
Ok(result)
}
pub async fn test_concurrent_determinism(&self) -> Result<DeterminismTestResult, Box<dyn std::error::Error>> {
let mut result = DeterminismTestResult::new("Concurrent Determinism".to_string());
let mut unique_outputs = HashMap::new();
let build_request = self.generate_simple_build_request();
let mut tasks = Vec::new();
for _ in 0..self.config.iterations {
let req = build_request.clone();
let task = tokio::spawn(async move {
let start_time = Instant::now();
let builder = Builder::new();
let xml_result = Self::build_xml_static(&builder, &req);
let build_time = start_time.elapsed().as_millis() as u64;
(xml_result, build_time)
});
tasks.push(task);
}
for task in tasks {
result.total_iterations += 1;
match task.await {
Ok((Ok(xml), build_time)) => {
result.successful_iterations += 1;
result.build_times_ms.push(build_time);
result.output_sizes.push(xml.len());
let output_hash = self.hash_xml(&xml);
*unique_outputs.entry(output_hash).or_insert(0) += 1;
},
Ok((Err(e), _)) => {
result.failed_iterations += 1;
result.errors.push(e.to_string());
},
Err(e) => {
result.failed_iterations += 1;
result.errors.push(format!("Task failed: {}", e));
}
}
}
result.unique_outputs = unique_outputs.len();
Ok(result)
}
pub async fn test_memory_constrained_determinism(&self) -> Result<DeterminismTestResult, Box<dyn std::error::Error>> {
let mut result = DeterminismTestResult::new("Memory-Constrained Determinism".to_string());
let mut unique_outputs = HashMap::new();
for _ in 0..self.config.iterations {
result.total_iterations += 1;
let _memory_pressure: Vec<Vec<u8>> = (0..100)
.map(|_| vec![0u8; 1024])
.collect();
let build_request = self.generate_simple_build_request();
let start_time = Instant::now();
match self.build_xml(&build_request) {
Ok(xml) => {
let build_time = start_time.elapsed().as_millis() as u64;
result.successful_iterations += 1;
result.build_times_ms.push(build_time);
result.output_sizes.push(xml.len());
let output_hash = self.hash_xml(&xml);
*unique_outputs.entry(output_hash).or_insert(0) += 1;
},
Err(e) => {
result.failed_iterations += 1;
result.errors.push(e.to_string());
}
}
}
result.unique_outputs = unique_outputs.len();
Ok(result)
}
pub async fn test_seeded_determinism(&self) -> Result<DeterminismTestResult, Box<dyn std::error::Error>> {
let mut result = DeterminismTestResult::new("Seeded Determinism".to_string());
let mut unique_outputs = HashMap::new();
let base_request = self.generate_simple_build_request();
for seed in 0..self.config.iterations {
result.total_iterations += 1;
let build_request = self.generate_seeded_variant(&base_request, seed as u64);
let start_time = Instant::now();
match self.build_xml(&build_request) {
Ok(xml) => {
let build_time = start_time.elapsed().as_millis() as u64;
result.successful_iterations += 1;
result.build_times_ms.push(build_time);
result.output_sizes.push(xml.len());
let output_hash = self.hash_xml(&xml);
*unique_outputs.entry(output_hash).or_insert(0) += 1;
},
Err(e) => {
result.failed_iterations += 1;
result.errors.push(e.to_string());
}
}
}
result.unique_outputs = unique_outputs.len();
Ok(result)
}
fn build_xml(&self, request: &BuildRequest) -> Result<String, Box<dyn std::error::Error>> {
let builder = Builder::new();
Self::build_xml_static(&builder, request)
}
fn build_xml_static(builder: &Builder, request: &BuildRequest) -> Result<String, Box<dyn std::error::Error>> {
Ok(format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><test>{}</test>", request.get_id()))
}
fn generate_simple_build_request(&self) -> BuildRequest {
BuildRequest::new_with_id("test_simple".to_string())
}
fn generate_complex_build_request(&self) -> BuildRequest {
BuildRequest::new_with_id("test_complex".to_string())
}
fn generate_seeded_variant(&self, base: &BuildRequest, seed: u64) -> BuildRequest {
BuildRequest::new_with_id(format!("test_seeded_{}", seed))
}
fn hash_xml(&self, xml: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
xml.hash(&mut hasher);
hasher.finish()
}
}
impl BuildRequest {
pub fn new_with_id(id: String) -> Self {
Self { id }
}
pub fn get_id(&self) -> &str {
&self.id
}
}
pub struct BuildRequest {
id: String,
}
impl Clone for BuildRequest {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
}
}
}
pub fn ddex_message_strategy() -> impl Strategy<Value = DdexMessage> {
prop::collection::vec(sound_recording_strategy(), 1..10)
.prop_map(|resources| DdexMessage {
message_id: format!("MSG_{}", uuid::Uuid::new_v4()),
resources,
})
}
pub fn sound_recording_strategy() -> impl Strategy<Value = SoundRecording> {
(
"[A-Z0-9]{1,50}",
"[A-Za-z0-9 ]{1,100}",
prop::option::of("PT[0-9]{1,2}M[0-9]{1,2}S"),
).prop_map(|(id, title, duration)| SoundRecording {
resource_id: id,
title,
duration,
})
}
#[derive(Debug, Clone)]
pub struct DdexMessage {
pub message_id: String,
pub resources: Vec<SoundRecording>,
}
#[derive(Debug, Clone)]
pub struct SoundRecording {
pub resource_id: String,
pub title: String,
pub duration: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_determinism_result_creation() {
let result = DeterminismTestResult::new("Test".to_string());
assert_eq!(result.test_name, "Test");
assert_eq!(result.total_iterations, 0);
assert!(!result.is_deterministic()); }
#[test]
fn test_determinism_metrics() {
let mut result = DeterminismTestResult::new("Test".to_string());
result.total_iterations = 10;
result.successful_iterations = 8;
result.failed_iterations = 2;
result.unique_outputs = 1;
result.build_times_ms = vec![10, 15, 12, 8, 20, 11, 9, 14];
assert!(result.is_deterministic());
assert_eq!(result.success_rate(), 0.8);
assert_eq!(result.average_build_time_ms(), 12.375);
}
#[tokio::test]
async fn test_basic_determinism_runner() {
let config = DeterminismTestConfig {
iterations: 5,
test_multithreading: false,
test_memory_constraints: false,
test_random_seeds: false,
max_complexity: 10,
};
let runner = DeterminismTestRunner::new(config);
let result = runner.test_basic_determinism().await.unwrap();
assert!(result.total_iterations > 0);
}
proptest! {
#[test]
fn test_property_based_determinism(message in ddex_message_strategy()) {
let message_id = &message.message_id;
assert!(!message_id.is_empty());
assert!(message.resources.len() > 0);
}
#[test]
fn test_sound_recording_properties(recording in sound_recording_strategy()) {
assert!(!recording.resource_id.is_empty());
assert!(!recording.title.is_empty());
assert!(recording.resource_id.len() <= 50);
assert!(recording.title.len() <= 100);
}
}
}