#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
use super::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use sublime_standard_tools::error::{FileSystemError, Result as StandardResult};
#[derive(Debug, Clone)]
struct MockFileSystem {
files: Arc<Mutex<HashMap<String, Vec<u8>>>>,
}
impl MockFileSystem {
fn new() -> Self {
Self { files: Arc::new(Mutex::new(HashMap::new())) }
}
fn normalize_path(path: &Path) -> String {
let path_str = path.to_string_lossy().replace('\\', "/");
if cfg!(windows)
&& path_str.len() >= 2
&& let Some(second_char) = path_str.chars().nth(1)
&& second_char == ':'
{
let drive = path_str.chars().next().unwrap().to_ascii_lowercase();
return format!("/{}{}", drive, &path_str[2..]);
}
path_str
}
fn add_file(&self, path: PathBuf, content: &str) {
let normalized = Self::normalize_path(&path);
self.files.lock().unwrap().insert(normalized, content.as_bytes().to_vec());
}
fn get_file(&self, path: &Path) -> Option<String> {
let normalized = Self::normalize_path(path);
self.files
.lock()
.unwrap()
.get(&normalized)
.map(|bytes| String::from_utf8_lossy(bytes).to_string())
}
fn file_exists(&self, path: &Path) -> bool {
let normalized = Self::normalize_path(path);
self.files.lock().unwrap().contains_key(&normalized)
}
fn list_all_files(&self) -> Vec<String> {
let files = self.files.lock().unwrap();
let mut keys: Vec<String> = files.keys().cloned().collect();
keys.sort();
keys
}
}
#[async_trait::async_trait]
impl AsyncFileSystem for MockFileSystem {
async fn read_file(&self, path: &Path) -> StandardResult<Vec<u8>> {
let normalized = Self::normalize_path(path);
self.files
.lock()
.unwrap()
.get(&normalized)
.cloned()
.ok_or_else(|| FileSystemError::NotFound { path: path.to_path_buf() }.into())
}
async fn write_file(&self, path: &Path, contents: &[u8]) -> StandardResult<()> {
let normalized = Self::normalize_path(path);
self.files.lock().unwrap().insert(normalized, contents.to_vec());
Ok(())
}
async fn read_file_string(&self, path: &Path) -> StandardResult<String> {
let bytes = self.read_file(path).await?;
String::from_utf8(bytes).map_err(|e| {
FileSystemError::Utf8Decode { path: path.to_path_buf(), message: e.to_string() }.into()
})
}
async fn write_file_string(&self, path: &Path, contents: &str) -> StandardResult<()> {
self.write_file(path, contents.as_bytes()).await
}
async fn create_dir_all(&self, _path: &Path) -> StandardResult<()> {
Ok(())
}
async fn remove(&self, path: &Path) -> StandardResult<()> {
let normalized_path = Self::normalize_path(path);
let mut files = self.files.lock().unwrap();
files.retain(|p, _| {
*p != normalized_path && !p.starts_with(&format!("{}/", normalized_path))
});
Ok(())
}
async fn exists(&self, path: &Path) -> bool {
let normalized_path = Self::normalize_path(path);
let files = self.files.lock().unwrap();
let normalized_path = normalized_path.trim_end_matches('/');
if files.contains_key(normalized_path) {
return true;
}
files.keys().any(|p| {
p.starts_with(normalized_path)
&& p.len() > normalized_path.len()
&& p.as_bytes().get(normalized_path.len()) == Some(&b'/')
})
}
async fn read_dir(&self, path: &Path) -> StandardResult<Vec<PathBuf>> {
let normalized_path = Self::normalize_path(path);
let files: Vec<PathBuf> = self
.files
.lock()
.unwrap()
.keys()
.filter(|p| p.starts_with(&normalized_path))
.map(PathBuf::from)
.collect();
Ok(files)
}
async fn walk_dir(&self, path: &Path) -> StandardResult<Vec<PathBuf>> {
self.read_dir(path).await
}
async fn metadata(&self, _path: &Path) -> StandardResult<std::fs::Metadata> {
Err(FileSystemError::Operation("Metadata not supported in mock".to_string()).into())
}
}
fn create_test_manager(config: BackupConfig) -> BackupManager<MockFileSystem> {
BackupManager::new(PathBuf::from("/workspace"), config, MockFileSystem::new())
}
#[tokio::test]
async fn test_path_normalization() {
let fs = MockFileSystem::new();
assert_eq!(
MockFileSystem::normalize_path(&PathBuf::from("/workspace/file.txt")),
"/workspace/file.txt"
);
assert_eq!(
MockFileSystem::normalize_path(&PathBuf::from("/workspace\\file.txt")),
"/workspace/file.txt"
);
assert_eq!(
MockFileSystem::normalize_path(&PathBuf::from("/workspace\\.workspace-backups\\file.txt")),
"/workspace/.workspace-backups/file.txt"
);
fs.add_file(PathBuf::from("/workspace/test.txt"), "content");
assert!(fs.file_exists(&PathBuf::from("/workspace/test.txt")));
assert!(fs.file_exists(&PathBuf::from("/workspace\\test.txt")));
}
#[tokio::test]
async fn test_directory_exists_edge_cases() {
let fs = MockFileSystem::new();
fs.add_file(PathBuf::from("/workspace/.workspace-backups/backup1/file1.txt"), "content1");
fs.add_file(PathBuf::from("/workspace/.workspace-backups/backup2/file2.txt"), "content2");
fs.add_file(PathBuf::from("/workspace/.workspace-backups-other/file3.txt"), "content3");
assert!(fs.exists(&PathBuf::from("/workspace/.workspace-backups")).await);
assert!(fs.exists(&PathBuf::from("/workspace/.workspace-backups/")).await);
assert!(fs.exists(&PathBuf::from("/workspace/.workspace-backups/backup1")).await);
assert!(fs.exists(&PathBuf::from("/workspace/.workspace-backups/backup2")).await);
assert!(fs.exists(&PathBuf::from("/workspace/.workspace-backups-other")).await);
assert!(!fs.exists(&PathBuf::from("/workspace/.workspace-backups-other/nonexistent")).await);
assert!(fs.exists(&PathBuf::from("/workspace\\.workspace-backups\\backup1")).await);
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_create_backup_success() {
let config = BackupConfig {
enabled: true,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: false,
max_backups: 5,
};
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
manager.fs.add_file(
manager.workspace_root.join("packages/core/package.json"),
r#"{"name": "@test/core"}"#,
);
let files = vec![PathBuf::from("package.json"), PathBuf::from("packages/core/package.json")];
let backup_id = manager.create_backup(&files, "upgrade").await.unwrap();
assert!(backup_id.contains("upgrade"));
assert!(backup_id.contains("-"));
let backup_path = manager.backup_path(&backup_id);
let expected_file1 = backup_path.join("package.json");
let expected_file2 = backup_path.join("packages/core/package.json");
assert!(
manager.fs.file_exists(&expected_file1),
"Expected file not found: {}\nNormalized: {}\nAll files: {:?}",
expected_file1.display(),
MockFileSystem::normalize_path(&expected_file1),
manager.fs.list_all_files()
);
assert!(
manager.fs.file_exists(&expected_file2),
"Expected file not found: {}\nNormalized: {}\nAll files: {:?}",
expected_file2.display(),
MockFileSystem::normalize_path(&expected_file2),
manager.fs.list_all_files()
);
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 1);
assert_eq!(backups[0].id, backup_id);
assert_eq!(backups[0].operation, "upgrade");
assert_eq!(backups[0].files.len(), 2);
assert!(!backups[0].success);
}
#[tokio::test]
async fn test_create_backup_disabled() {
let config = BackupConfig {
enabled: false,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: false,
max_backups: 5,
};
let manager = create_test_manager(config);
let files = vec![PathBuf::from("package.json")];
let result = manager.create_backup(&files, "upgrade").await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::BackupFailed { reason, .. } => {
assert!(reason.contains("disabled"));
}
_ => panic!("Expected BackupFailed error"),
}
}
#[tokio::test]
async fn test_create_backup_nonexistent_file() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let files = vec![PathBuf::from("/workspace/nonexistent.json")];
let result = manager.create_backup(&files, "upgrade").await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::FileSystemError { reason, .. } => {
assert!(reason.contains("does not exist"));
}
_ => panic!("Expected FileSystemError"),
}
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_restore_backup_success() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let files = vec![PathBuf::from("package.json")];
let backup_id = manager.create_backup(&files, "upgrade").await.unwrap();
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "modified"}"#);
manager.restore_backup(&backup_id).await.unwrap();
let content = manager.fs.get_file(&manager.workspace_root.join("package.json")).unwrap();
assert!(content.contains(r#""name": "test""#));
}
#[tokio::test]
async fn test_restore_backup_nonexistent() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let result = manager.restore_backup("nonexistent-backup").await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::NoBackup { .. } => {}
_ => panic!("Expected NoBackup error"),
}
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_restore_last_backup() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "v1"}"#);
let _backup1 =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "v2"}"#);
let _backup2 =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "modified"}"#);
manager.restore_last_backup().await.unwrap();
let content = manager.fs.get_file(&manager.workspace_root.join("package.json")).unwrap();
assert!(content.contains(r#""name": "v2""#));
}
#[tokio::test]
async fn test_restore_last_backup_no_backups() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let result = manager.restore_last_backup().await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::NoBackup { .. } => {}
_ => panic!("Expected NoBackup error"),
}
}
#[tokio::test]
async fn test_list_backups() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup1 = manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let backup2 =
manager.create_backup(&[PathBuf::from("package.json")], "rollback").await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 2);
assert_eq!(backups[0].id, backup2);
assert_eq!(backups[1].id, backup1);
}
#[tokio::test]
async fn test_list_backups_empty() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let backups = manager.list_backups().await.unwrap();
assert!(backups.is_empty());
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_delete_backup() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup_id =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 1);
manager.delete_backup(&backup_id).await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert!(backups.is_empty());
}
#[tokio::test]
async fn test_delete_backup_nonexistent() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let result = manager.delete_backup("nonexistent-backup").await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::NoBackup { .. } => {}
_ => panic!("Expected NoBackup error"),
}
}
#[tokio::test]
async fn test_mark_success() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup_id =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert!(!backups[0].success);
manager.mark_success(&backup_id).await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert!(backups[0].success);
}
#[tokio::test]
async fn test_mark_success_nonexistent() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
let result = manager.mark_success("nonexistent-backup").await;
assert!(result.is_err());
match result.unwrap_err() {
UpgradeError::NoBackup { .. } => {}
_ => panic!("Expected NoBackup error"),
}
}
#[tokio::test]
async fn test_cleanup_removes_successful_backups() {
let config = BackupConfig {
enabled: true,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: false,
max_backups: 5,
};
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup_id =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
manager.mark_success(&backup_id).await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 1);
manager.cleanup_old_backups().await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert!(backups.is_empty());
}
#[tokio::test]
async fn test_cleanup_keeps_successful_backups() {
let config = BackupConfig {
enabled: true,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: true,
max_backups: 5,
};
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup_id =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
manager.mark_success(&backup_id).await.unwrap();
manager.cleanup_old_backups().await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 1);
}
#[tokio::test]
async fn test_cleanup_removes_old_backups() {
let config = BackupConfig {
enabled: true,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: true,
max_backups: 3,
};
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
for _ in 0..5 {
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 3); }
#[tokio::test]
async fn test_cleanup_priority_removes_successful_before_count() {
let config = BackupConfig {
enabled: true,
backup_dir: ".workspace-backups".to_string(),
keep_after_success: false,
max_backups: 2,
};
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup1 = manager.create_backup(&[PathBuf::from("package.json")], "backup1").await.unwrap();
manager.mark_success(&backup1).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let backup2 = manager.create_backup(&[PathBuf::from("package.json")], "backup2").await.unwrap();
manager.mark_success(&backup2).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let _backup3 =
manager.create_backup(&[PathBuf::from("package.json")], "backup3").await.unwrap();
manager.cleanup_old_backups().await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups.len(), 1); assert!(!backups[0].success);
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_relative_path_handling() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let files = vec![PathBuf::from("package.json")];
let backup_id = manager.create_backup(&files, "upgrade").await.unwrap();
let backups = manager.list_backups().await.unwrap();
assert_eq!(backups[0].files[0], manager.workspace_root.join("package.json"));
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "modified"}"#);
manager.restore_backup(&backup_id).await.unwrap();
let content = manager.fs.get_file(&manager.workspace_root.join("package.json")).unwrap();
assert!(content.contains(r#""name": "test""#));
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_nested_directory_structure() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(
manager.workspace_root.join("packages/core/src/lib/package.json"),
r#"{"name": "@test/core"}"#,
);
let files = vec![PathBuf::from("packages/core/src/lib/package.json")];
let backup_id = manager.create_backup(&files, "upgrade").await.unwrap();
let backup_path = manager.backup_path(&backup_id);
assert!(manager.fs.file_exists(&backup_path.join("packages/core/src/lib/package.json")));
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "Windows path normalization issues - tracked in WOR-TSK-141"
)]
async fn test_multiple_files_backup_and_restore() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "root"}"#);
manager.fs.add_file(manager.workspace_root.join("packages/a/package.json"), r#"{"name": "a"}"#);
manager.fs.add_file(manager.workspace_root.join("packages/b/package.json"), r#"{"name": "b"}"#);
let files = vec![
PathBuf::from("package.json"),
PathBuf::from("packages/a/package.json"),
PathBuf::from("packages/b/package.json"),
];
let backup_id = manager.create_backup(&files, "upgrade").await.unwrap();
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "root-mod"}"#);
manager
.fs
.add_file(manager.workspace_root.join("packages/a/package.json"), r#"{"name": "a-mod"}"#);
manager
.fs
.add_file(manager.workspace_root.join("packages/b/package.json"), r#"{"name": "b-mod"}"#);
manager.restore_backup(&backup_id).await.unwrap();
assert!(
manager
.fs
.get_file(&manager.workspace_root.join("package.json"))
.unwrap()
.contains(r#""name": "root""#)
);
assert!(
manager
.fs
.get_file(&manager.workspace_root.join("packages/a/package.json"))
.unwrap()
.contains(r#""name": "a""#)
);
assert!(
manager
.fs
.get_file(&manager.workspace_root.join("packages/b/package.json"))
.unwrap()
.contains(r#""name": "b""#)
);
}
#[tokio::test]
async fn test_backup_metadata_serialization() {
let metadata = BackupMetadata {
id: "2024-01-15T10-30-45-upgrade".to_string(),
created_at: Utc::now(),
operation: "upgrade".to_string(),
files: vec![PathBuf::from("/workspace/package.json")],
success: true,
};
let json = serde_json::to_string(&metadata).unwrap();
let deserialized: BackupMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(metadata.id, deserialized.id);
assert_eq!(metadata.operation, deserialized.operation);
assert_eq!(metadata.files, deserialized.files);
assert_eq!(metadata.success, deserialized.success);
}
#[tokio::test]
async fn test_backup_id_format() {
let config = BackupConfig::default();
let manager = create_test_manager(config);
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let backup_id =
manager.create_backup(&[PathBuf::from("package.json")], "upgrade").await.unwrap();
assert!(backup_id.ends_with("-upgrade"));
assert!(backup_id.contains('T'));
assert_eq!(backup_id.matches('-').count(), 6); }
#[tokio::test]
async fn test_concurrent_backups() {
let config = BackupConfig::default();
let manager = Arc::new(create_test_manager(config));
manager.fs.add_file(manager.workspace_root.join("package.json"), r#"{"name": "test"}"#);
let mut handles = vec![];
for i in 0..5 {
let manager_clone = Arc::clone(&manager);
let handle = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(i * 10)).await;
manager_clone.create_backup(&[PathBuf::from("package.json")], "upgrade").await
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
assert_eq!(results.len(), 5);
for result in results {
assert!(result.unwrap().is_ok());
}
let backups = manager.list_backups().await.unwrap();
assert!(backups.len() <= 5); }