use sorting_race::models::config::{Distribution, FairnessMode, RunConfiguration};
use sorting_race::models::traits::{FairnessModel, Sorter, StepResult, Telemetry};
use sorting_race::services::fairness::comparison::ComparisonBudget;
use sorting_race::services::generator::ArrayGenerator;
use sorting_race::services::sorters::{
bubble::BubbleSort, heap::HeapSort, insertion::InsertionSort, merge::MergeSort,
quick::QuickSort, selection::SelectionSort,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct SortingSnapshot {
timestamp: u64,
config: RunConfiguration,
algorithm_states: Vec<AlgorithmState>,
progress: ProgressStats,
metadata: SnapshotMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct AlgorithmState {
name: String,
array: Vec<i32>,
telemetry: TelemetrySnapshot,
is_complete: bool,
step_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TelemetrySnapshot {
total_comparisons: usize,
total_moves: usize,
array_accesses: usize,
custom_metrics: HashMap<String, i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct ProgressStats {
completed_algorithms: usize,
total_algorithms: usize,
total_steps: usize,
completion_percentage: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct SnapshotMetadata {
format_version: String,
description: String,
tags: Vec<String>,
}
struct SnapshotManager {
temp_dir: TempDir,
}
impl SnapshotManager {
fn new() -> anyhow::Result<Self> {
let temp_dir = tempfile::tempdir()?;
Ok(Self { temp_dir })
}
fn save_snapshot(&self, snapshot: &SortingSnapshot, filename: &str) -> anyhow::Result<String> {
let file_path = self.temp_dir.path().join(filename);
let json_data = serde_json::to_string_pretty(snapshot)?;
fs::write(&file_path, json_data)?;
Ok(file_path.to_string_lossy().to_string())
}
fn load_snapshot(&self, filename: &str) -> anyhow::Result<SortingSnapshot> {
let file_path = self.temp_dir.path().join(filename);
let json_data = fs::read_to_string(&file_path)?;
let snapshot: SortingSnapshot = serde_json::from_str(&json_data)?;
Ok(snapshot)
}
fn snapshot_exists(&self, filename: &str) -> bool {
let file_path = self.temp_dir.path().join(filename);
file_path.exists()
}
fn get_snapshot_path(&self, filename: &str) -> String {
let file_path = self.temp_dir.path().join(filename);
file_path.to_string_lossy().to_string()
}
fn list_snapshots(&self) -> anyhow::Result<Vec<String>> {
let mut snapshots = Vec::new();
for entry in fs::read_dir(self.temp_dir.path())? {
let entry = entry?;
if let Some(filename) = entry.file_name().to_str() {
if filename.ends_with(".json") {
snapshots.push(filename.to_string());
}
}
}
snapshots.sort();
Ok(snapshots)
}
}
fn create_test_algorithms() -> Vec<Box<dyn Sorter>> {
vec![
Box::new(BubbleSort::new()),
Box::new(InsertionSort::new()),
Box::new(SelectionSort::new()),
Box::new(QuickSort::new()),
Box::new(HeapSort::new()),
Box::new(MergeSort::new()),
]
}
fn create_test_config() -> RunConfiguration {
RunConfiguration {
array_size: 20,
distribution: Distribution::Random,
fairness_mode: FairnessMode::ComparisonBudget { k: 10 },
seed: Some(42),
}
}
fn telemetry_to_snapshot(telemetry: &Telemetry) -> TelemetrySnapshot {
TelemetrySnapshot {
total_comparisons: telemetry.total_comparisons,
total_moves: telemetry.total_moves,
array_accesses: telemetry.array_accesses.unwrap_or(0),
custom_metrics: HashMap::new(), }
}
fn create_snapshot_from_algorithms(
algorithms: &[Box<dyn Sorter>],
config: &RunConfiguration,
total_steps: usize,
) -> SortingSnapshot {
let algorithm_states: Vec<AlgorithmState> = algorithms
.iter()
.map(|alg| {
let telemetry = alg.get_telemetry();
AlgorithmState {
name: alg.name().to_string(),
array: alg.get_array().to_vec(),
telemetry: telemetry_to_snapshot(&telemetry),
is_complete: alg.is_complete(),
step_count: telemetry.total_comparisons + telemetry.total_moves, }
})
.collect();
let completed_count = algorithm_states.iter().filter(|s| s.is_complete).count();
let total_count = algorithm_states.len();
let completion_percentage = if total_count > 0 {
(completed_count as f64 / total_count as f64) * 100.0
} else {
0.0
};
SortingSnapshot {
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
config: config.clone(),
algorithm_states,
progress: ProgressStats {
completed_algorithms: completed_count,
total_algorithms: total_count,
total_steps,
completion_percentage,
},
metadata: SnapshotMetadata {
format_version: "1.0.0".to_string(),
description: "Test snapshot for sorting race".to_string(),
tags: vec!["test".to_string(), "integration".to_string()],
},
}
}
fn validate_snapshot_structure(snapshot: &SortingSnapshot) -> anyhow::Result<()> {
assert!(snapshot.timestamp > 0, "Timestamp should be positive");
assert!(
snapshot.timestamp < 2_000_000_000, "Timestamp seems too far in the future"
);
assert!(
!snapshot.algorithm_states.is_empty(),
"Should have at least one algorithm state"
);
for state in &snapshot.algorithm_states {
assert!(!state.name.is_empty(), "Algorithm name should not be empty");
assert!(!state.array.is_empty(), "Array should not be empty");
assert!(
state.telemetry.total_comparisons >= 0,
"Comparisons should be non-negative"
);
assert!(
state.telemetry.total_moves >= 0,
"Moves should be non-negative"
);
}
assert_eq!(
snapshot.progress.total_algorithms,
snapshot.algorithm_states.len(),
"Total algorithms count should match states length"
);
assert!(
snapshot.progress.completed_algorithms <= snapshot.progress.total_algorithms,
"Completed count should not exceed total"
);
assert!(
snapshot.progress.completion_percentage >= 0.0
&& snapshot.progress.completion_percentage <= 100.0,
"Completion percentage should be between 0 and 100"
);
assert!(
!snapshot.metadata.format_version.is_empty(),
"Format version should not be empty"
);
assert!(
!snapshot.metadata.description.is_empty(),
"Description should not be empty"
);
Ok(())
}
#[cfg(test)]
mod snapshot_tests {
use super::*;
#[test]
fn test_snapshot_creation_and_basic_serialization() {
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let snapshot = create_snapshot_from_algorithms(&algorithms, &config, 0);
validate_snapshot_structure(&snapshot).expect("Snapshot should be valid");
let json = serde_json::to_string(&snapshot).expect("Should serialize to JSON");
assert!(!json.is_empty(), "JSON should not be empty");
assert!(json.contains("algorithm_states"), "JSON should contain algorithm states");
assert!(json.contains("timestamp"), "JSON should contain timestamp");
}
#[test]
fn test_snapshot_save_and_load_roundtrip() {
let manager = SnapshotManager::new().expect("Should create snapshot manager");
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let fairness_model = ComparisonBudget::new(5);
for _ in 0..3 {
let budgets = fairness_model.allocate_budget(&algorithms);
for (i, algorithm) in algorithms.iter_mut().enumerate() {
if budgets[i] > 0 && !algorithm.is_complete() {
algorithm.step(budgets[i]);
}
}
}
let original_snapshot = create_snapshot_from_algorithms(&algorithms, &config, 3);
let filename = "test_snapshot.json";
let saved_path = manager
.save_snapshot(&original_snapshot, filename)
.expect("Should save snapshot");
assert!(manager.snapshot_exists(filename), "Snapshot file should exist");
assert!(Path::new(&saved_path).exists(), "File should exist at saved path");
let loaded_snapshot = manager
.load_snapshot(filename)
.expect("Should load snapshot");
assert_eq!(original_snapshot, loaded_snapshot, "Loaded snapshot should match original");
validate_snapshot_structure(&loaded_snapshot).expect("Loaded snapshot should be valid");
}
#[test]
fn test_snapshot_contains_all_required_fields() {
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let snapshot = create_snapshot_from_algorithms(&algorithms, &config, 0);
assert!(snapshot.timestamp > 0, "Should have valid timestamp");
assert!(!snapshot.algorithm_states.is_empty(), "Should have algorithm states");
assert_eq!(snapshot.progress.total_algorithms, algorithms.len(), "Should track correct algorithm count");
assert!(!snapshot.metadata.format_version.is_empty(), "Should have format version");
assert!(!snapshot.metadata.description.is_empty(), "Should have description");
for state in &snapshot.algorithm_states {
assert!(!state.name.is_empty(), "Algorithm should have name");
assert!(!state.array.is_empty(), "Algorithm should have array data");
assert!(state.telemetry.total_comparisons >= 0, "Should have comparison count");
assert!(state.telemetry.total_moves >= 0, "Should have move count");
}
let json = serde_json::to_string_pretty(&snapshot).expect("Should serialize");
let required_fields = [
"timestamp", "config", "algorithm_states", "progress", "metadata",
"name", "array", "telemetry", "is_complete",
"total_comparisons", "total_moves", "format_version"
];
for field in &required_fields {
assert!(json.contains(field), "JSON should contain field: {}", field);
}
}
#[test]
fn test_multiple_snapshots_without_conflicts() {
let manager = SnapshotManager::new().expect("Should create snapshot manager");
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let fairness_model = ComparisonBudget::new(5);
let mut snapshots = Vec::new();
for step in 0..5 {
if step > 0 {
let budgets = fairness_model.allocate_budget(&algorithms);
for (i, algorithm) in algorithms.iter_mut().enumerate() {
if budgets[i] > 0 && !algorithm.is_complete() {
algorithm.step(budgets[i]);
}
}
}
let snapshot = create_snapshot_from_algorithms(&algorithms, &config, step);
let filename = format!("snapshot_step_{}.json", step);
manager
.save_snapshot(&snapshot, &filename)
.expect("Should save snapshot");
snapshots.push((filename, snapshot));
}
for i in 0..snapshots.len() {
let (filename, _) = &snapshots[i];
assert!(manager.snapshot_exists(filename), "Snapshot {} should exist", i);
}
for (filename, original) in &snapshots {
let loaded = manager
.load_snapshot(filename)
.expect("Should load snapshot");
assert_eq!(*original, loaded, "Snapshot {} should match", filename);
}
for i in 1..snapshots.len() {
let (_, prev_snapshot) = &snapshots[i - 1];
let (_, curr_snapshot) = &snapshots[i];
let mut progress_made = false;
for j in 0..prev_snapshot.algorithm_states.len() {
let prev_state = &prev_snapshot.algorithm_states[j];
let curr_state = &curr_snapshot.algorithm_states[j];
if curr_state.telemetry.total_comparisons > prev_state.telemetry.total_comparisons ||
curr_state.telemetry.total_moves > prev_state.telemetry.total_moves {
progress_made = true;
break;
}
}
}
let snapshot_list = manager.list_snapshots().expect("Should list snapshots");
assert_eq!(snapshot_list.len(), 5, "Should have 5 snapshot files");
for i in 0..snapshot_list.len() {
let expected = format!("snapshot_step_{}.json", i);
assert_eq!(snapshot_list[i], expected, "Snapshot should be in order");
}
}
#[test]
fn test_loaded_state_matches_saved_state_exactly() {
let manager = SnapshotManager::new().expect("Should create snapshot manager");
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let fairness_model = ComparisonBudget::new(8);
for _ in 0..10 {
let budgets = fairness_model.allocate_budget(&algorithms);
for (i, algorithm) in algorithms.iter_mut().enumerate() {
if budgets[i] > 0 && !algorithm.is_complete() {
algorithm.step(budgets[i]);
}
}
}
let original_snapshot = create_snapshot_from_algorithms(&algorithms, &config, 10);
let filename = "complex_state_test.json";
manager
.save_snapshot(&original_snapshot, filename)
.expect("Should save complex snapshot");
let loaded_snapshot = manager
.load_snapshot(filename)
.expect("Should load complex snapshot");
assert_eq!(original_snapshot.timestamp, loaded_snapshot.timestamp);
assert_eq!(original_snapshot.config, loaded_snapshot.config);
assert_eq!(original_snapshot.progress, loaded_snapshot.progress);
assert_eq!(original_snapshot.metadata, loaded_snapshot.metadata);
assert_eq!(original_snapshot.algorithm_states.len(), loaded_snapshot.algorithm_states.len());
for (orig, loaded) in original_snapshot.algorithm_states.iter().zip(loaded_snapshot.algorithm_states.iter()) {
assert_eq!(orig.name, loaded.name);
assert_eq!(orig.array, loaded.array);
assert_eq!(orig.is_complete, loaded.is_complete);
assert_eq!(orig.step_count, loaded.step_count);
assert_eq!(orig.telemetry.total_comparisons, loaded.telemetry.total_comparisons);
assert_eq!(orig.telemetry.total_moves, loaded.telemetry.total_moves);
assert_eq!(orig.telemetry.array_accesses, loaded.telemetry.array_accesses);
assert_eq!(orig.telemetry.custom_metrics, loaded.telemetry.custom_metrics);
}
}
#[test]
fn test_snapshot_with_completed_algorithms() {
let manager = SnapshotManager::new().expect("Should create snapshot manager");
let config = RunConfiguration {
array_size: 5, distribution: Distribution::Random,
fairness_mode: FairnessMode::ComparisonBudget { k: 20 },
seed: Some(123),
};
let mut algorithms = vec![
Box::new(BubbleSort::new()) as Box<dyn Sorter>,
Box::new(InsertionSort::new()) as Box<dyn Sorter>,
];
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let fairness_model = ComparisonBudget::new(10);
let mut steps = 0;
while algorithms.iter().all(|alg| !alg.is_complete()) && steps < 100 {
let budgets = fairness_model.allocate_budget(&algorithms);
for (i, algorithm) in algorithms.iter_mut().enumerate() {
if budgets[i] > 0 {
algorithm.step(budgets[i]);
}
}
steps += 1;
}
let snapshot = create_snapshot_from_algorithms(&algorithms, &config, steps);
assert!(snapshot.progress.completed_algorithms > 0, "Should have completed algorithms");
assert!(snapshot.progress.completion_percentage > 0.0, "Should have non-zero completion percentage");
let filename = "completed_algorithms_test.json";
manager
.save_snapshot(&snapshot, filename)
.expect("Should save snapshot with completed algorithms");
let loaded_snapshot = manager
.load_snapshot(filename)
.expect("Should load snapshot with completed algorithms");
assert_eq!(snapshot.progress.completed_algorithms, loaded_snapshot.progress.completed_algorithms);
assert_eq!(snapshot.progress.completion_percentage, loaded_snapshot.progress.completion_percentage);
for (orig, loaded) in snapshot.algorithm_states.iter().zip(loaded_snapshot.algorithm_states.iter()) {
assert_eq!(orig.is_complete, loaded.is_complete);
}
}
#[test]
fn test_snapshot_json_format_validation() {
let manager = SnapshotManager::new().expect("Should create snapshot manager");
let config = create_test_config();
let mut algorithms = create_test_algorithms();
let generator = ArrayGenerator::new();
let test_array = generator.generate(&config);
for algorithm in &mut algorithms {
algorithm.reset(test_array.clone());
}
let snapshot = create_snapshot_from_algorithms(&algorithms, &config, 0);
let filename = "format_validation_test.json";
let file_path = manager
.save_snapshot(&snapshot, filename)
.expect("Should save snapshot");
let json_content = fs::read_to_string(&file_path).expect("Should read JSON file");
let json_value: serde_json::Value = serde_json::from_str(&json_content)
.expect("Should be valid JSON");
assert!(json_value.is_object(), "Root should be an object");
let obj = json_value.as_object().unwrap();
let expected_root_keys = ["timestamp", "config", "algorithm_states", "progress", "metadata"];
for key in &expected_root_keys {
assert!(obj.contains_key(key), "Should contain key: {}", key);
}
assert!(obj["algorithm_states"].is_array(), "algorithm_states should be array");
let states = obj["algorithm_states"].as_array().unwrap();
assert!(!states.is_empty(), "Should have algorithm states");
for state in states {
assert!(state.is_object(), "Each state should be an object");
let state_obj = state.as_object().unwrap();
let expected_state_keys = ["name", "array", "telemetry", "is_complete", "step_count"];
for key in &expected_state_keys {
assert!(state_obj.contains_key(key), "State should contain key: {}", key);
}
assert!(state_obj["telemetry"].is_object(), "telemetry should be object");
let telemetry = state_obj["telemetry"].as_object().unwrap();
let expected_telemetry_keys = ["total_comparisons", "total_moves", "array_accesses", "custom_metrics"];
for key in &expected_telemetry_keys {
assert!(telemetry.contains_key(key), "Telemetry should contain key: {}", key);
}
}
assert!(obj["progress"].is_object(), "progress should be object");
let progress = obj["progress"].as_object().unwrap();
let expected_progress_keys = ["completed_algorithms", "total_algorithms", "total_steps", "completion_percentage"];
for key in &expected_progress_keys {
assert!(progress.contains_key(key), "Progress should contain key: {}", key);
}
assert!(obj["metadata"].is_object(), "metadata should be object");
let metadata = obj["metadata"].as_object().unwrap();
let expected_metadata_keys = ["format_version", "description", "tags"];
for key in &expected_metadata_keys {
assert!(metadata.contains_key(key), "Metadata should contain key: {}", key);
}
}
}