prvng_platform/orchestrator/tests/migration_tests.rs

597 lines
21 KiB
Rust
Raw Normal View History

2025-10-07 10:59:52 +01:00
//! Comprehensive migration tests for storage backend transitions
//!
//! This module tests data migration between all supported storage backends,
//! ensuring data integrity, rollback functionality, and performance characteristics.
use std::sync::Arc;
use tempfile::TempDir;
use tokio_test;
// Import test helpers and orchestrator types
mod helpers;
use helpers::{TestEnvironment, TestDataGenerator, StorageAssertions};
use orchestrator::{TaskStatus, WorkflowTask};
use orchestrator::storage::{StorageConfig, create_storage};
use orchestrator::migration::{
StorageMigrator, MigrationConfig, MigrationOptions, MigrationStatus,
RollbackManager, RollbackConfig, RollbackStrategy, RollbackStatus
};
/// Test basic migration between filesystem and SurrealDB embedded
#[cfg(feature = "surrealdb")]
#[tokio::test]
async fn test_filesystem_to_embedded_migration() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.surrealdb_embedded_config().unwrap();
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(10);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Add some audit entries and metrics
let audit_entry = gen.audit_entry(test_tasks[0].id.clone());
source_storage.record_audit_entry(audit_entry).await.unwrap();
let metric = gen.metric("test_metric", 42.0);
source_storage.record_metric(metric).await.unwrap();
// Configure migration
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
verify_integrity: true,
create_backup: true,
..Default::default()
},
};
// Execute migration
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify migration success
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 10);
assert_eq!(report.statistics.tasks_failed, 0);
assert_eq!(report.errors.len(), 0);
// Verify data in target storage
let target_storage = create_storage(report.target_backend.parse().unwrap()).await.unwrap();
StorageAssertions::assert_task_count(&target_storage, 10).await.unwrap();
}
/// Test migration with progress callback
#[tokio::test]
async fn test_migration_with_progress_tracking() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap(); // Different directory
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(5);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Track progress updates
let progress_updates = Arc::new(std::sync::Mutex::new(Vec::new()));
let progress_updates_clone = progress_updates.clone();
let progress_callback = Arc::new(move |progress: &orchestrator::migration::MigrationProgress| {
progress_updates_clone.lock().unwrap().push((
progress.phase.clone(),
progress.percentage,
progress.message.clone(),
));
});
// Configure migration
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
batch_size: 2,
..Default::default()
},
};
// Execute migration with progress tracking
let migrator = StorageMigrator::new(migration_config)
.with_progress_callback(progress_callback);
let report = migrator.execute().await.unwrap();
// Verify migration success
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 5);
// Verify progress was tracked
let updates = progress_updates.lock().unwrap();
assert!(!updates.is_empty());
// Check that we received updates for all phases
let phases: Vec<String> = updates.iter().map(|(phase, _, _)| phase.clone()).collect();
assert!(phases.contains(&"validation".to_string()));
assert!(phases.contains(&"connection".to_string()));
assert!(phases.contains(&"discovery".to_string()));
assert!(phases.contains(&"migration".to_string()));
}
/// Test migration with filtering options
#[tokio::test]
async fn test_migration_with_filtering() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap();
// Setup source data with different statuses
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let pending_tasks = (0..3).map(|_| gen.workflow_task_with_status(TaskStatus::Pending)).collect::<Vec<_>>();
let completed_tasks = (0..2).map(|_| gen.workflow_task_with_status(TaskStatus::Completed)).collect::<Vec<_>>();
let failed_tasks = (0..1).map(|_| gen.workflow_task_with_status(TaskStatus::Failed)).collect::<Vec<_>>();
for task in &pending_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
for task in &completed_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
for task in &failed_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Configure migration to only migrate pending and completed tasks
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
status_filter: Some(vec![TaskStatus::Pending, TaskStatus::Completed]),
verify_integrity: false, // Skip integrity check for filtered migration
..Default::default()
},
};
// Execute migration
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify migration success
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 5); // 3 pending + 2 completed
assert_eq!(report.statistics.tasks_failed, 0);
// Verify target storage contains only filtered tasks
let target_storage = create_storage(report.target_backend.parse().unwrap()).await.unwrap();
StorageAssertions::assert_task_count(&target_storage, 5).await.unwrap();
let failed_tasks_in_target = target_storage.list_tasks(Some(TaskStatus::Failed)).await.unwrap();
assert_eq!(failed_tasks_in_target.len(), 0);
}
/// Test dry run migration
#[tokio::test]
async fn test_dry_run_migration() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap();
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(5);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Configure dry run migration
let migration_config = MigrationConfig {
source_config,
target_config: target_config.clone(),
options: MigrationOptions {
dry_run: true,
..Default::default()
},
};
// Execute dry run
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify dry run completed successfully
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 5);
// Verify target storage is empty (no actual migration occurred)
let target_storage = create_storage(target_config).await.unwrap();
target_storage.init().await.unwrap();
StorageAssertions::assert_task_count(&target_storage, 0).await.unwrap();
}
/// Test migration failure and rollback
#[tokio::test]
async fn test_migration_failure_and_rollback() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
// Create an invalid target config to force migration failure
let mut target_config = env.filesystem_config().unwrap();
target_config.data_dir = "/invalid/path/that/does/not/exist".to_string();
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(3);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Configure migration with backup for rollback
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
create_backup: true,
..Default::default()
},
};
// Execute migration (should fail)
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify migration failed
assert_eq!(report.status, MigrationStatus::Failed);
assert!(!report.errors.is_empty());
// Check if automatic rollback was attempted
assert!(!report.warnings.is_empty());
let rollback_warning = report.warnings.iter()
.find(|w| w.contains("Automatic rollback executed"));
assert!(rollback_warning.is_some());
}
/// Test manual rollback process
#[tokio::test]
async fn test_manual_rollback_process() {
let rollback_config = RollbackConfig {
backup_path: "/tmp/test_backup.json".to_string(),
migration_id: "test_migration_123".to_string(),
rollback_strategy: RollbackStrategy::Manual,
};
let rollback_manager = RollbackManager::new(rollback_config);
// Create a mock target storage for rollback
let mut env = TestEnvironment::new();
let target_config = env.filesystem_config().unwrap();
let target_storage = create_storage(target_config).await.unwrap();
target_storage.init().await.unwrap();
// Execute manual rollback
let rollback_report = rollback_manager.execute_rollback(&target_storage).await.unwrap();
// Verify rollback completed (manual strategy just generates instructions)
assert_eq!(rollback_report.status, RollbackStatus::Completed);
assert!(!rollback_report.actions_performed.is_empty());
// Check that instructions were generated
let instruction_action = rollback_report.actions_performed.iter()
.find(|a| a.action.contains("manual_instructions"));
assert!(instruction_action.is_some());
}
/// Test migration between different backend combinations
#[cfg(feature = "surrealdb")]
#[tokio::test]
async fn test_all_backend_combinations() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let configs = env.all_storage_configs();
// Test migration between each pair of backends
for (i, source_config) in configs.iter().enumerate() {
for (j, target_config) in configs.iter().enumerate() {
if i == j {
continue; // Skip same backend migrations
}
println!("Testing migration from {} to {}",
source_config.storage_type, target_config.storage_type);
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(3);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Configure migration
let migration_config = MigrationConfig {
source_config: source_config.clone(),
target_config: target_config.clone(),
options: MigrationOptions {
dry_run: false,
verify_integrity: true,
create_backup: false, // Skip backup for test speed
..Default::default()
},
};
// Execute migration
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify migration success
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 3);
assert_eq!(report.statistics.tasks_failed, 0);
println!("✓ Migration from {} to {} successful",
source_config.storage_type, target_config.storage_type);
}
}
}
/// Test large dataset migration performance
#[tokio::test]
async fn test_large_dataset_migration_performance() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap();
// Setup large dataset
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let task_count = 1000;
println!("Setting up {} tasks for migration...", task_count);
let batch_tasks: Vec<(WorkflowTask, u8)> = (0..task_count)
.map(|i| (gen.workflow_task(), (i % 10 + 1) as u8))
.collect();
source_storage.enqueue_batch(batch_tasks).await.unwrap();
// Configure migration with optimized batch size
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
batch_size: 100,
verify_integrity: false, // Skip for performance
create_backup: false,
..Default::default()
},
};
// Execute migration with timing
let start = std::time::Instant::now();
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
let duration = start.elapsed();
// Verify migration success
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, task_count);
println!("Migration of {} tasks completed in {:?}", task_count, duration);
println!("Throughput: {:.2} tasks/second", report.statistics.throughput_tasks_per_second);
// Performance should be reasonable (adjust based on requirements)
assert!(duration.as_secs() < 30, "Migration took too long: {:?}", duration);
assert!(report.statistics.throughput_tasks_per_second > 10.0, "Throughput too low");
}
/// Test migration with data integrity verification
#[tokio::test]
async fn test_migration_data_integrity() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap();
// Setup source data with complex tasks
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let mut complex_task = gen.workflow_task();
complex_task.args = vec!["arg1".to_string(), "arg2".to_string(), "arg3".to_string()];
complex_task.dependencies = vec!["dep1".to_string(), "dep2".to_string()];
complex_task.status = TaskStatus::Running;
complex_task.started_at = Some(chrono::Utc::now());
complex_task.output = Some("Complex output data".to_string());
source_storage.enqueue(complex_task.clone(), 5).await.unwrap();
// Configure migration with integrity verification
let migration_config = MigrationConfig {
source_config,
target_config: target_config.clone(),
options: MigrationOptions {
dry_run: false,
verify_integrity: true,
..Default::default()
},
};
// Execute migration
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Verify migration success with integrity check
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 1);
assert_eq!(report.errors.len(), 0);
// Verify complex data was preserved
let target_storage = create_storage(target_config).await.unwrap();
target_storage.init().await.unwrap();
let migrated_task = target_storage.get_task(&complex_task.id).await.unwrap().unwrap();
assert_eq!(migrated_task.args, complex_task.args);
assert_eq!(migrated_task.dependencies, complex_task.dependencies);
assert_eq!(migrated_task.status, complex_task.status);
assert_eq!(migrated_task.output, complex_task.output);
}
/// Test migration rollback with target storage cleanup
#[tokio::test]
async fn test_rollback_with_target_cleanup() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
// Create a valid target config
let target_config = env.filesystem_config().unwrap();
let target_storage = create_storage(target_config.clone()).await.unwrap();
target_storage.init().await.unwrap();
// Add some test data to target storage (simulating partial migration)
let test_tasks = gen.workflow_tasks_batch(3);
for task in &test_tasks {
target_storage.enqueue(task.clone(), 1).await.unwrap();
}
StorageAssertions::assert_task_count(&target_storage, 3).await.unwrap();
// Configure rollback to clear target storage
let rollback_config = RollbackConfig {
backup_path: "/tmp/nonexistent_backup.json".to_string(),
migration_id: "test_migration_456".to_string(),
rollback_strategy: RollbackStrategy::ClearTargetRestoreSource,
};
let rollback_manager = RollbackManager::new(rollback_config);
// Execute rollback
let rollback_report = rollback_manager.execute_rollback(&target_storage).await.unwrap();
// Note: The current implementation doesn't support task deletion
// This test verifies the rollback process recognizes the limitation
assert_eq!(rollback_report.status, RollbackStatus::PartiallyCompleted);
assert!(!rollback_report.errors.is_empty());
let limitation_error = rollback_report.errors.iter()
.find(|e| e.contains("TaskStorage trait lacks delete_task method"));
assert!(limitation_error.is_some());
}
/// Test migration continuation after non-critical errors
#[tokio::test]
async fn test_migration_continue_on_error() {
let mut env = TestEnvironment::new();
let gen = TestDataGenerator::new();
let source_config = env.filesystem_config().unwrap();
let target_config = env.filesystem_config().unwrap();
// Setup source data
let source_storage = create_storage(source_config.clone()).await.unwrap();
source_storage.init().await.unwrap();
let test_tasks = gen.workflow_tasks_batch(5);
for task in &test_tasks {
source_storage.enqueue(task.clone(), 1).await.unwrap();
}
// Configure migration to continue on error
let migration_config = MigrationConfig {
source_config,
target_config,
options: MigrationOptions {
dry_run: false,
continue_on_error: true,
max_retries: 1,
verify_integrity: false,
..Default::default()
},
};
// Execute migration
let migrator = StorageMigrator::new(migration_config);
let report = migrator.execute().await.unwrap();
// Migration should complete despite potential errors
assert_eq!(report.status, MigrationStatus::Completed);
assert_eq!(report.statistics.tasks_migrated, 5);
}
/// Test migration utility functions for common scenarios
#[cfg(feature = "surrealdb")]
#[tokio::test]
async fn test_migration_utility_functions() {
let mut env = TestEnvironment::new();
let source_dir = env.create_temp_dir().unwrap();
let target_dir = env.create_temp_dir().unwrap();
// Test filesystem to embedded utility
let migration_options = MigrationOptions {
dry_run: true,
..Default::default()
};
let migrator = StorageMigrator::filesystem_to_embedded(
&source_dir.to_string_lossy(),
&target_dir.to_string_lossy(),
Some(migration_options),
);
// Verify configuration was set correctly
let report = migrator.execute().await.unwrap();
assert_eq!(report.source_backend, "filesystem");
assert_eq!(report.target_backend, "surrealdb-embedded");
assert_eq!(report.status, MigrationStatus::Completed);
// Test embedded to server utility
let migrator = StorageMigrator::embedded_to_server(
&target_dir.to_string_lossy(),
"memory://test",
"test",
"test",
Some(MigrationOptions {
dry_run: true,
..Default::default()
}),
);
let report = migrator.execute().await.unwrap();
assert_eq!(report.source_backend, "surrealdb-embedded");
assert_eq!(report.target_backend, "surrealdb-server");
assert_eq!(report.status, MigrationStatus::Completed);
}