//! Checkpoint archival and backup/restore operation tests //! //! Tests comprehensive checkpoint management scenarios including: //! - Large checkpoint uploads //! - Checkpoint retrieval with verification //! - Backup and restore workflows //! - Concurrent checkpoint operations //! - Error handling for checkpoint operations use object_store::memory::InMemory; use object_store::ObjectStore; use std::sync::Arc; use storage::object_store_backend::ObjectStoreBackend; use storage::Storage; /// Helper to create test backend with in-memory store fn create_test_backend() -> ObjectStoreBackend { let in_memory_store: Arc = Arc::new(InMemory::new()); storage::object_store_backend::test_helpers::new_for_testing( in_memory_store, "checkpoints-bucket".to_string(), ) } #[tokio::test] async fn test_checkpoint_upload_and_download() { let backend = create_test_backend(); // Simulate checkpoint data (10MB) let checkpoint_data = vec![0xAB; 10 * 1024 * 1024]; let checkpoint_path = backend.get_checkpoint_path("mamba2", "epoch_100"); // Upload checkpoint backend .store(&checkpoint_path, &checkpoint_data) .await .expect("Failed to upload checkpoint"); // Download checkpoint let downloaded = backend .retrieve(&checkpoint_path) .await .expect("Failed to download checkpoint"); assert_eq!( downloaded.len(), checkpoint_data.len(), "Downloaded checkpoint size mismatch" ); assert_eq!( downloaded, checkpoint_data, "Downloaded checkpoint data mismatch" ); } #[tokio::test] async fn test_checkpoint_metadata_storage() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("dqn", "epoch_50"); let metadata_path = backend.get_metadata_path("dqn", "v1.0"); // Store checkpoint let checkpoint_data = b"checkpoint weights"; backend .store(&checkpoint_path, checkpoint_data) .await .unwrap(); // Store metadata let metadata = serde_json::json!({ "model": "dqn", "epoch": 50, "loss": 0.123, "accuracy": 0.95 }); let metadata_bytes = serde_json::to_vec(&metadata).unwrap(); backend .store(&metadata_path, &metadata_bytes) .await .unwrap(); // Verify both exist assert!(backend.exists(&checkpoint_path).await.unwrap()); assert!(backend.exists(&metadata_path).await.unwrap()); // Retrieve and verify metadata let retrieved_metadata = backend.retrieve(&metadata_path).await.unwrap(); let parsed: serde_json::Value = serde_json::from_slice(&retrieved_metadata).unwrap(); assert_eq!(parsed["model"], "dqn"); assert_eq!(parsed["epoch"], 50); } #[tokio::test] async fn test_checkpoint_backup_workflow() { let backend = create_test_backend(); // Primary checkpoint let primary_path = "models/ppo/checkpoints/primary/epoch_100.safetensors"; let checkpoint_data = vec![0xCD; 5 * 1024 * 1024]; // 5MB // Store primary checkpoint backend.store(primary_path, &checkpoint_data).await.unwrap(); // Create backup let backup_path = "models/ppo/backups/epoch_100_backup.safetensors"; let retrieved = backend.retrieve(primary_path).await.unwrap(); backend.store(backup_path, &retrieved).await.unwrap(); // Verify both exist assert!(backend.exists(primary_path).await.unwrap()); assert!(backend.exists(backup_path).await.unwrap()); // Verify backup integrity let backup_data = backend.retrieve(backup_path).await.unwrap(); assert_eq!(backup_data, checkpoint_data); } #[tokio::test] async fn test_checkpoint_restore_from_backup() { let backend = create_test_backend(); let backup_path = "backups/tft/epoch_200.safetensors"; let restore_path = "models/tft/checkpoints/epoch_200_restored.safetensors"; let backup_data = vec![0xEF; 8 * 1024 * 1024]; // 8MB // Store backup backend.store(backup_path, &backup_data).await.unwrap(); // Simulate restore operation let restored_data = backend.retrieve(backup_path).await.unwrap(); backend.store(restore_path, &restored_data).await.unwrap(); // Verify restored checkpoint let final_data = backend.retrieve(restore_path).await.unwrap(); assert_eq!(final_data.len(), backup_data.len()); assert_eq!(final_data, backup_data); } #[tokio::test] async fn test_checkpoint_versioning() { let backend = create_test_backend(); let model_name = "mamba2"; let versions = ["v1.0", "v1.1", "v2.0"]; // Store multiple checkpoint versions for version in &versions { let path = backend.get_checkpoint_path(model_name, version); let data = format!("checkpoint_{}", version).into_bytes(); backend.store(&path, &data).await.unwrap(); } // List all checkpoints let prefix = format!("models/{}/checkpoints/", model_name); let checkpoints = backend.list(&prefix).await.unwrap(); assert_eq!(checkpoints.len(), versions.len()); // Verify each version exists for version in &versions { let path = backend.get_checkpoint_path(model_name, version); assert!(backend.exists(&path).await.unwrap()); } } #[tokio::test] async fn test_checkpoint_deletion() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("dqn", "old_epoch_10"); let checkpoint_data = b"old checkpoint data"; // Store checkpoint backend .store(&checkpoint_path, checkpoint_data) .await .unwrap(); assert!(backend.exists(&checkpoint_path).await.unwrap()); // Delete checkpoint let deleted = backend.delete(&checkpoint_path).await.unwrap(); assert!(deleted); // Verify deletion assert!(!backend.exists(&checkpoint_path).await.unwrap()); } #[tokio::test] async fn test_checkpoint_cleanup_old_versions() { let backend = create_test_backend(); let model_name = "ppo"; let max_checkpoints = 3; // Store 5 checkpoints (should keep only 3 latest) for i in 1..=5 { let path = backend.get_checkpoint_path(model_name, &format!("epoch_{}", i * 10)); let data = format!("checkpoint_{}", i).into_bytes(); backend.store(&path, &data).await.unwrap(); } let prefix = format!("models/{}/checkpoints/", model_name); let all_checkpoints = backend.list(&prefix).await.unwrap(); assert_eq!(all_checkpoints.len(), 5); // Simulate cleanup: delete oldest checkpoints let mut to_delete = all_checkpoints.clone(); to_delete.sort(); while to_delete.len() > max_checkpoints { let oldest = to_delete.remove(0); backend.delete(&oldest).await.unwrap(); } // Verify only max_checkpoints remain let remaining = backend.list(&prefix).await.unwrap(); assert_eq!(remaining.len(), max_checkpoints); } #[tokio::test] async fn test_concurrent_checkpoint_operations() { let backend = Arc::new(create_test_backend()); let mut handles = vec![]; // Concurrent checkpoint uploads for i in 0..5 { let backend = Arc::clone(&backend); let handle = tokio::spawn(async move { let path = format!("models/concurrent_test/checkpoints/epoch_{}.bin", i); let data = vec![i as u8; 1024 * 1024]; // 1MB each backend.store(&path, &data).await.unwrap(); // Verify upload let retrieved = backend.retrieve(&path).await.unwrap(); assert_eq!(retrieved.len(), data.len()); }); handles.push(handle); } for handle in handles { handle.await.unwrap(); } // Verify all checkpoints exist let checkpoints = backend .list("models/concurrent_test/checkpoints/") .await .unwrap(); assert_eq!(checkpoints.len(), 5); } #[tokio::test] async fn test_checkpoint_integrity_verification() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("tft", "epoch_150"); let checkpoint_data = vec![0x42; 15 * 1024 * 1024]; // 15MB // Calculate expected checksum (SHA-256) use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(&checkpoint_data); let expected_checksum = format!("{:x}", hasher.finalize()); // Store checkpoint backend .store(&checkpoint_path, &checkpoint_data) .await .unwrap(); // Retrieve and verify checksum let retrieved = backend.retrieve(&checkpoint_path).await.unwrap(); let mut hasher = Sha256::new(); hasher.update(&retrieved); let actual_checksum = format!("{:x}", hasher.finalize()); assert_eq!(actual_checksum, expected_checksum); } #[tokio::test] async fn test_checkpoint_partial_upload_failure() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("mamba2", "epoch_partial"); let checkpoint_data = vec![0xAA; 20 * 1024 * 1024]; // 20MB // Upload checkpoint let result = backend.store(&checkpoint_path, &checkpoint_data).await; assert!(result.is_ok()); // Verify metadata reflects correct size let metadata = backend.metadata(&checkpoint_path).await.unwrap(); assert_eq!(metadata.size, checkpoint_data.len() as u64); } #[tokio::test] async fn test_checkpoint_list_with_pagination() { let backend = create_test_backend(); // Store 20 checkpoints for i in 1..=20 { let path = format!("models/pagination_test/checkpoints/epoch_{}.bin", i); backend.store(&path, &vec![i as u8; 1024]).await.unwrap(); } // List all checkpoints let all_checkpoints = backend .list("models/pagination_test/checkpoints/") .await .unwrap(); assert_eq!(all_checkpoints.len(), 20); } #[tokio::test] async fn test_checkpoint_empty_content() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("empty_test", "epoch_0"); let empty_data = b""; // Store empty checkpoint backend.store(&checkpoint_path, empty_data).await.unwrap(); // Retrieve and verify let retrieved = backend.retrieve(&checkpoint_path).await.unwrap(); assert_eq!(retrieved.len(), 0); // Verify metadata let metadata = backend.metadata(&checkpoint_path).await.unwrap(); assert_eq!(metadata.size, 0); } #[tokio::test] async fn test_checkpoint_overwrite_protection() { let backend = create_test_backend(); let checkpoint_path = backend.get_checkpoint_path("overwrite_test", "epoch_100"); let original_data = b"original checkpoint v1"; let new_data = b"updated checkpoint v2"; // Store original backend .store(&checkpoint_path, original_data) .await .unwrap(); // Overwrite (should succeed in S3) backend.store(&checkpoint_path, new_data).await.unwrap(); // Verify overwrite let retrieved = backend.retrieve(&checkpoint_path).await.unwrap(); assert_eq!(retrieved, new_data); } #[tokio::test] async fn test_checkpoint_metadata_size_validation() { let backend = create_test_backend(); let sizes = [ 1024, // 1KB 1024 * 1024, // 1MB 10 * 1024 * 1024, // 10MB 100 * 1024 * 1024, // 100MB (large checkpoint) ]; for (idx, &size) in sizes.iter().enumerate() { let path = format!("models/size_test/checkpoint_{}.bin", idx); let data = vec![0xFF; size]; backend.store(&path, &data).await.unwrap(); let metadata = backend.metadata(&path).await.unwrap(); assert_eq!( metadata.size, size as u64, "Size mismatch for checkpoint {}", idx ); } }