use crate::diff::{format_colored_diff, format_unified_diff, should_use_color};
use crate::error::{Result, SpliceError};
use crate::graph::MagellanIntegration;
use chrono::Utc;
use magellan::references::ReferenceFact;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub fn generate_preview_diff(
file_path: &Path,
original_content: &str,
modified_content: &str,
) -> String {
format_unified_diff(
original_content,
modified_content,
&file_path.display().to_string(),
3, )
}
pub fn generate_colored_preview(
file_path: &Path,
original_content: &str,
modified_content: &str,
) -> String {
let use_color = should_use_color();
if use_color {
format_colored_diff(original_content, modified_content, use_color)
} else {
format_unified_diff(
original_content,
modified_content,
&file_path.display().to_string(),
3,
)
}
}
pub fn simulate_replacements_content(
content: &str,
references: &[ReferenceFact],
_old_name: &str,
new_name: &str,
) -> Result<String> {
let content_bytes = content.as_bytes();
let new_name_bytes = new_name.as_bytes();
let mut current_content = content_bytes.to_vec();
for reference in references {
match replace_at_span(¤t_content, reference, new_name_bytes) {
Ok(new_content) => {
current_content = new_content;
}
Err(e) => {
return Err(SpliceError::Other(format!(
"Failed to simulate replacement at {}..{}: {}",
reference.byte_start, reference.byte_end, e
)));
}
}
}
String::from_utf8(current_content).map_err(|e| SpliceError::InvalidUtf8 {
file: PathBuf::from("<preview>"),
source: e.utf8_error(),
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameBackupManifest {
pub operation_id: String,
pub timestamp: String,
pub files: HashMap<String, String>,
}
pub fn create_rename_backup(
workspace_root: &Path,
symbol_id: &str,
files_to_backup: &[PathBuf],
) -> Result<PathBuf> {
let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
let operation_id = format!("rename-{}-{}", symbol_id, timestamp);
let backups_base = workspace_root.join(".splice/backups");
fs::create_dir_all(&backups_base).map_err(|e| SpliceError::Io {
path: backups_base.clone(),
source: e,
})?;
let backup_dir = backups_base.join(&operation_id);
fs::create_dir(&backup_dir).map_err(|e| SpliceError::Io {
path: backup_dir.clone(),
source: e,
})?;
let mut manifest = RenameBackupManifest {
operation_id: operation_id.clone(),
timestamp: Utc::now().to_rfc3339(),
files: HashMap::new(),
};
for file_path in files_to_backup {
let relative_path = file_path.strip_prefix(workspace_root).map_err(|_| {
SpliceError::Other(format!(
"File {} is not under workspace root {}",
file_path.display(),
workspace_root.display()
))
})?;
let backup_file_path = backup_dir.join(relative_path);
if let Some(parent) = backup_file_path.parent() {
fs::create_dir_all(parent).map_err(|e| SpliceError::Io {
path: parent.to_path_buf(),
source: e,
})?;
}
fs::copy(file_path, &backup_file_path).map_err(|e| SpliceError::Io {
path: backup_file_path.clone(),
source: e,
})?;
let checksum = sha256_checksum(file_path)?;
manifest
.files
.insert(relative_path.display().to_string(), checksum);
}
let manifest_path = backup_dir.join("manifest.json");
let manifest_json = serde_json::to_string_pretty(&manifest)
.map_err(|e| SpliceError::Other(format!("Failed to serialize backup manifest: {}", e)))?;
fs::write(&manifest_path, manifest_json).map_err(|e| SpliceError::Io {
path: manifest_path.clone(),
source: e,
})?;
Ok(backup_dir)
}
fn sha256_checksum(file_path: &Path) -> Result<String> {
let mut hasher = Sha256::new();
let content = fs::read(file_path).map_err(|e| SpliceError::Io {
path: file_path.to_path_buf(),
source: e,
})?;
hasher.update(&content);
Ok(format!("{:x}", hasher.finalize()))
}
#[derive(Debug)]
pub struct RenameTransaction {
backup_dir: Option<PathBuf>,
workspace_root: Option<PathBuf>,
modified_files: Vec<PathBuf>,
}
impl RenameTransaction {
pub fn new() -> Self {
Self {
backup_dir: None,
workspace_root: None,
modified_files: Vec::new(),
}
}
pub fn with_backup(mut self, backup_dir: PathBuf, workspace_root: PathBuf) -> Self {
self.backup_dir = Some(backup_dir);
self.workspace_root = Some(workspace_root);
self
}
pub fn track_modified(&mut self, file: PathBuf) {
self.modified_files.push(file);
}
pub fn rollback(self) -> Result<()> {
if let (Some(backup_dir), Some(workspace_root)) = (self.backup_dir, self.workspace_root) {
let manifest_path = backup_dir.join("manifest.json");
if manifest_path.exists() {
let manifest_json =
fs::read_to_string(&manifest_path).map_err(|e| SpliceError::Io {
path: manifest_path.clone(),
source: e,
})?;
let manifest: RenameBackupManifest =
serde_json::from_str(&manifest_json).map_err(|e| {
SpliceError::Other(format!("Failed to parse backup manifest: {}", e))
})?;
for (relative_path, _checksum) in manifest.files {
let backup_file = backup_dir.join(&relative_path);
let original_file = workspace_root.join(&relative_path);
if backup_file.exists() {
fs::copy(&backup_file, &original_file).map_err(|e| SpliceError::Io {
path: original_file.clone(),
source: e,
})?;
}
}
}
}
Ok(())
}
pub fn modified_count(&self) -> usize {
self.modified_files.len()
}
pub fn modified_files(&self) -> &[PathBuf] {
&self.modified_files
}
}
impl Default for RenameTransaction {
fn default() -> Self {
Self::new()
}
}
pub fn apply_with_rollback<F>(
file_path: &Path,
modify: F,
transaction: &mut RenameTransaction,
) -> Result<()>
where
F: FnOnce() -> Result<Vec<u8>>,
{
let new_content = modify()?;
fs::write(file_path, new_content).map_err(|e| SpliceError::Io {
path: file_path.to_path_buf(),
source: e,
})?;
transaction.track_modified(file_path.to_path_buf());
Ok(())
}
pub fn replace_at_span(content: &[u8], span: &ReferenceFact, new_name: &[u8]) -> Result<Vec<u8>> {
if span.byte_start >= content.len() || span.byte_end > content.len() {
return Err(SpliceError::InvalidSpan {
file: span.file_path.clone(),
start: span.byte_start,
end: span.byte_end,
file_size: content.len(),
});
}
MagellanIntegration::validate_utf8_span(
content,
span.byte_start,
span.byte_end,
&span.file_path,
)?;
let mut result = Vec::with_capacity(content.len() + new_name.len());
result.extend_from_slice(&content[..span.byte_start]);
result.extend_from_slice(new_name);
result.extend_from_slice(&content[span.byte_end..]);
Ok(result)
}
pub fn apply_replacements_in_file(
file_path: &Path,
_old_name: &str,
new_name: &str,
references: &[ReferenceFact],
) -> Result<usize> {
let content = fs::read(file_path).map_err(|e| SpliceError::Io {
path: file_path.to_path_buf(),
source: e,
})?;
let new_name_bytes = new_name.as_bytes();
let mut current_content = content;
let mut replacements = 0;
for reference in references {
match replace_at_span(¤t_content, reference, new_name_bytes) {
Ok(new_content) => {
current_content = new_content;
replacements += 1;
}
Err(e) => {
return Err(SpliceError::Other(format!(
"Failed to replace in {} at {}..{}: {}",
file_path.display(),
reference.byte_start,
reference.byte_end,
e
)));
}
}
}
fs::write(file_path, current_content).map_err(|e| SpliceError::Io {
path: file_path.to_path_buf(),
source: e,
})?;
Ok(replacements)
}
pub fn group_references_by_file(
references: &[ReferenceFact],
) -> HashMap<PathBuf, Vec<ReferenceFact>> {
let mut grouped: HashMap<PathBuf, Vec<ReferenceFact>> = HashMap::new();
for reference in references {
grouped
.entry(reference.file_path.clone())
.or_insert_with(Vec::new)
.push(reference.clone());
}
for refs in grouped.values_mut() {
refs.sort_by(|a, b| b.byte_start.cmp(&a.byte_start));
}
grouped
}
pub fn simulate_replacements(references: &[ReferenceFact]) -> HashMap<PathBuf, usize> {
let mut simulation: HashMap<PathBuf, usize> = HashMap::new();
for reference in references {
*simulation.entry(reference.file_path.clone()).or_insert(0) += 1;
}
simulation
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_reference(file_path: &str, byte_start: usize, byte_end: usize) -> ReferenceFact {
ReferenceFact {
file_path: PathBuf::from(file_path),
referenced_symbol: "old_name".to_string(),
byte_start,
byte_end,
start_line: 1,
start_col: byte_start,
end_line: 1,
end_col: byte_end,
}
}
#[test]
fn test_replace_at_span_basic() {
let content = b"fn old_name() { old_name(); }";
let span = create_test_reference("test.rs", 3, 11);
let new_name = b"new_name";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"fn new_name() { old_name(); }");
}
#[test]
fn test_replace_at_span_different_length() {
let content = b"fn foo() {}";
let span = create_test_reference("test.rs", 3, 6);
let new_name = b"bar";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"fn bar() {}");
}
#[test]
fn test_replace_at_span_longer_name() {
let content = b"fn foo() {}";
let span = create_test_reference("test.rs", 3, 6);
let new_name = b"baz_qux";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"fn baz_qux() {}");
}
#[test]
fn test_replace_at_span_shorter_name() {
let content = b"function foo() {}";
let span = create_test_reference("test.rs", 9, 12);
let new_name = b"x";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"function x() {}");
}
#[test]
fn test_replace_at_span_invalid_start() {
let content = b"fn foo() {}";
let span = create_test_reference("test.rs", 100, 105);
let new_name = b"bar";
let result = replace_at_span(content, &span, new_name);
assert!(result.is_err());
match result.unwrap_err() {
SpliceError::InvalidSpan { start, .. } => assert_eq!(start, 100),
_ => panic!("Expected InvalidSpan error"),
}
}
#[test]
fn test_replace_at_span_invalid_end() {
let content = b"fn foo() {}";
let span = create_test_reference("test.rs", 0, 100);
let new_name = b"bar";
let result = replace_at_span(content, &span, new_name);
assert!(result.is_err());
match result.unwrap_err() {
SpliceError::InvalidSpan { end, .. } => assert_eq!(end, 100),
_ => panic!("Expected InvalidSpan error"),
}
}
#[test]
fn test_replace_at_span_empty_replacement() {
let content = b"fn foo() {}";
let span = create_test_reference("test.rs", 3, 6);
let new_name = b"";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"fn () {}");
}
#[test]
fn test_group_references_by_file() {
let references = vec![
create_test_reference("/src/a.rs", 100, 103),
create_test_reference("/src/b.rs", 50, 53),
create_test_reference("/src/a.rs", 20, 23),
create_test_reference("/src/b.rs", 10, 13),
];
let grouped = group_references_by_file(&references);
assert_eq!(grouped.len(), 2);
assert!(grouped.contains_key(PathBuf::from("/src/a.rs").as_path()));
assert!(grouped.contains_key(PathBuf::from("/src/b.rs").as_path()));
let a_refs = grouped.get(&PathBuf::from("/src/a.rs")).unwrap();
assert_eq!(a_refs[0].byte_start, 100);
assert_eq!(a_refs[1].byte_start, 20);
let b_refs = grouped.get(&PathBuf::from("/src/b.rs")).unwrap();
assert_eq!(b_refs[0].byte_start, 50);
assert_eq!(b_refs[1].byte_start, 10);
}
#[test]
fn test_simulate_replacements() {
let references = vec![
create_test_reference("/src/a.rs", 100, 103),
create_test_reference("/src/b.rs", 50, 53),
create_test_reference("/src/a.rs", 20, 23),
];
let simulation = simulate_replacements(&references);
assert_eq!(simulation.len(), 2);
assert_eq!(simulation.get(&PathBuf::from("/src/a.rs")), Some(&2));
assert_eq!(simulation.get(&PathBuf::from("/src/b.rs")), Some(&1));
}
#[test]
fn test_utf8_multibyte_character_replacement() {
let content = "fn foo() { // 世界 }".as_bytes();
let span = create_test_reference("test.rs", 3, 6);
let new_name = b"bar";
let result = replace_at_span(content, &span, new_name).unwrap();
assert_eq!(result, b"fn bar() { // \xe4\xb8\x96\xe7\x95\x8c }");
}
#[test]
fn test_apply_replacements_in_file_integration() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let initial_content = b"fn old_name() {\n old_name();\n old_name();\n}\n";
fs::write(file_path, initial_content).unwrap();
let references = vec![
create_test_reference(file_path.to_str().unwrap(), 36, 44),
create_test_reference(file_path.to_str().unwrap(), 20, 28),
];
let count =
apply_replacements_in_file(file_path, "old_name", "new_name", &references).unwrap();
assert_eq!(count, 2);
let result_content = fs::read_to_string(file_path).unwrap();
assert_eq!(
result_content,
"fn old_name() {\n new_name();\n new_name();\n}\n"
);
}
#[test]
fn test_apply_replacements_with_multibyte_utf8() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let initial_content = "fn foo() { // 世界\n foo(); // 世界\n}".as_bytes();
fs::write(file_path, initial_content).unwrap();
let references = vec![create_test_reference(file_path.to_str().unwrap(), 3, 6)];
let count = apply_replacements_in_file(file_path, "foo", "bar", &references).unwrap();
assert_eq!(count, 1);
let result_content = fs::read_to_string(file_path).unwrap();
assert!(result_content.contains("世界"));
assert!(result_content.contains("bar()"));
}
#[test]
fn test_multiple_replacements_same_file() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let initial_content = b"a = foo + foo * foo;";
fs::write(file_path, initial_content).unwrap();
let references = vec![
create_test_reference(file_path.to_str().unwrap(), 16, 19),
create_test_reference(file_path.to_str().unwrap(), 10, 13),
create_test_reference(file_path.to_str().unwrap(), 4, 7),
];
let count = apply_replacements_in_file(file_path, "foo", "bar", &references).unwrap();
assert_eq!(count, 3);
let result_content = fs::read_to_string(file_path).unwrap();
assert_eq!(result_content, "a = bar + bar * bar;");
}
#[test]
fn test_apply_replacements_empty_list() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let initial_content = b"fn foo() {}";
fs::write(file_path, initial_content).unwrap();
let references: Vec<ReferenceFact> = vec![];
let count = apply_replacements_in_file(file_path, "foo", "bar", &references).unwrap();
assert_eq!(count, 0);
let result_content = fs::read_to_string(file_path).unwrap();
assert_eq!(result_content, "fn foo() {}");
}
#[test]
fn test_generate_preview_diff() {
let original = "fn foo() {\n println!(\"foo\");\n}\n";
let modified = "fn bar() {\n println!(\"bar\");\n}\n";
let file_path = PathBuf::from("test.rs");
let diff = generate_preview_diff(&file_path, original, modified);
assert!(diff.contains("--- a/test.rs"));
assert!(diff.contains("+++ b/test.rs"));
assert!(diff.contains("-fn foo()"));
assert!(diff.contains("+fn bar()"));
assert!(diff.contains("- println!(\"foo\");"));
assert!(diff.contains("+ println!(\"bar\");"));
}
#[test]
fn test_generate_preview_diff_no_changes() {
let content = "fn foo() {}\n";
let file_path = PathBuf::from("test.rs");
let diff = generate_preview_diff(&file_path, content, content);
assert!(diff.is_empty());
}
#[test]
fn test_generate_colored_preview() {
let original = "fn foo() {}\n";
let modified = "fn bar() {}\n";
let file_path = PathBuf::from("test.rs");
let colored = generate_colored_preview(&file_path, original, modified);
assert!(!colored.is_empty());
if !colored.contains('\x1b') {
assert!(colored.contains("-fn foo()"));
assert!(colored.contains("+fn bar()"));
}
}
#[test]
fn test_simulate_replacements_content() {
let content = "fn foo() {\n foo();\n}\n";
let file_path_str = "test.rs";
let references = vec![
ReferenceFact {
file_path: PathBuf::from(file_path_str),
referenced_symbol: "foo".to_string(),
byte_start: 15,
byte_end: 18,
start_line: 2,
start_col: 4,
end_line: 2,
end_col: 7,
},
ReferenceFact {
file_path: PathBuf::from(file_path_str),
referenced_symbol: "foo".to_string(),
byte_start: 3,
byte_end: 6,
start_line: 1,
start_col: 3,
end_line: 1,
end_col: 6,
},
];
let result = simulate_replacements_content(content, &references, "foo", "bar").unwrap();
assert_eq!(result, "fn bar() {\n bar();\n}\n");
}
#[test]
fn test_simulate_replacements_content_preserves_multibyte() {
let content = "fn foo() { // 世界\n}\n";
let references = vec![ReferenceFact {
file_path: PathBuf::from("test.rs"),
referenced_symbol: "foo".to_string(),
byte_start: 3,
byte_end: 6,
start_line: 1,
start_col: 3,
end_line: 1,
end_col: 6,
}];
let result = simulate_replacements_content(content, &references, "foo", "bar").unwrap();
assert!(result.contains("世界"));
assert!(result.contains("bar()"));
}
#[test]
fn test_create_rename_backup() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_root = temp_dir.path();
let file1 = workspace_root.join("src").join("main.rs");
let file2 = workspace_root.join("src").join("lib.rs");
fs::create_dir_all(file1.parent().unwrap()).unwrap();
fs::write(&file1, "fn foo() {}\n").unwrap();
fs::write(&file2, "fn bar() {}\n").unwrap();
let backup_dir = create_rename_backup(
workspace_root,
"test_symbol",
&[file1.clone(), file2.clone()],
)
.unwrap();
assert!(backup_dir.exists());
assert!(backup_dir.starts_with(workspace_root.join(".splice/backups")));
let manifest_path = backup_dir.join("manifest.json");
assert!(manifest_path.exists());
let manifest_json = fs::read_to_string(&manifest_path).unwrap();
let manifest: RenameBackupManifest = serde_json::from_str(&manifest_json).unwrap();
assert!(manifest.operation_id.starts_with("rename-test_symbol-"));
assert_eq!(manifest.files.len(), 2);
assert!(manifest.files.contains_key("src/main.rs"));
assert!(manifest.files.contains_key("src/lib.rs"));
let backup_file1 = backup_dir.join("src").join("main.rs");
let backup_file2 = backup_dir.join("src").join("lib.rs");
assert!(backup_file1.exists());
assert!(backup_file2.exists());
let original_content = fs::read_to_string(&file1).unwrap();
let backup_content = fs::read_to_string(&backup_file1).unwrap();
assert_eq!(original_content, backup_content);
}
#[test]
fn test_create_rename_backup_nested_files() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_root = temp_dir.path();
let file1 = workspace_root.join("src").join("api").join("handlers.rs");
let file2 = workspace_root.join("tests").join("integration_test.rs");
fs::create_dir_all(file1.parent().unwrap()).unwrap();
fs::create_dir_all(file2.parent().unwrap()).unwrap();
fs::write(&file1, "pub fn handler() {}\n").unwrap();
fs::write(&file2, "#[test]\nfn test() {}\n").unwrap();
let backup_dir = create_rename_backup(
workspace_root,
"nested_test",
&[file1.clone(), file2.clone()],
)
.unwrap();
let backup_file1 = backup_dir.join("src").join("api").join("handlers.rs");
let backup_file2 = backup_dir.join("tests").join("integration_test.rs");
assert!(backup_file1.exists());
assert!(backup_file2.exists());
let manifest_path = backup_dir.join("manifest.json");
let manifest: RenameBackupManifest =
serde_json::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap();
assert!(manifest.files.contains_key("src/api/handlers.rs"));
assert!(manifest.files.contains_key("tests/integration_test.rs"));
}
#[test]
fn test_sha256_checksum() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
fs::write(file_path, "test content").unwrap();
let checksum = sha256_checksum(file_path).unwrap();
assert_eq!(
checksum,
"6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72"
);
assert_eq!(checksum.len(), 64); }
#[test]
fn test_rename_transaction_new() {
let txn = RenameTransaction::new();
assert!(txn.backup_dir.is_none());
assert!(txn.workspace_root.is_none());
assert_eq!(txn.modified_count(), 0);
}
#[test]
fn test_rename_transaction_default() {
let txn = RenameTransaction::default();
assert_eq!(txn.modified_count(), 0);
}
#[test]
fn test_rename_transaction_with_backup() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_root = temp_dir.path();
let backup_dir = workspace_root.join(".splice/backups/test-backup");
let txn =
RenameTransaction::new().with_backup(backup_dir.clone(), workspace_root.to_path_buf());
assert!(txn.backup_dir.as_ref().is_some());
assert_eq!(txn.backup_dir.as_ref().unwrap(), &backup_dir);
assert!(txn.workspace_root.as_ref().is_some());
}
#[test]
fn test_rename_transaction_track_modified() {
let mut txn = RenameTransaction::new();
txn.track_modified(PathBuf::from("/path/to/file1.rs"));
txn.track_modified(PathBuf::from("/path/to/file2.rs"));
assert_eq!(txn.modified_count(), 2);
assert_eq!(
txn.modified_files(),
&[
PathBuf::from("/path/to/file1.rs"),
PathBuf::from("/path/to/file2.rs")
]
);
}
#[test]
fn test_rename_transaction_rollback() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let workspace_root = temp_dir.path();
let file_path = workspace_root.join("test.rs");
let original_content = "fn original() {}\n";
fs::write(&file_path, original_content).unwrap();
let backup_dir = workspace_root.join(".splice/backups/test-rollback");
fs::create_dir_all(&backup_dir).unwrap();
let backup_file = backup_dir.join("test.rs");
fs::write(&backup_file, original_content).unwrap();
let manifest = RenameBackupManifest {
operation_id: "test-rollback".to_string(),
timestamp: Utc::now().to_rfc3339(),
files: HashMap::from([("test.rs".to_string(), "dummy_checksum".to_string())]),
};
let manifest_path = backup_dir.join("manifest.json");
fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest).unwrap(),
)
.unwrap();
fs::write(&file_path, "fn modified() {}\n").unwrap();
let txn = RenameTransaction::new().with_backup(backup_dir, workspace_root.to_path_buf());
txn.rollback().unwrap();
let restored_content = fs::read_to_string(&file_path).unwrap();
assert_eq!(restored_content, original_content);
}
#[test]
fn test_apply_with_rollback_success() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let mut txn = RenameTransaction::new();
apply_with_rollback(file_path, || Ok(b"new content".to_vec()), &mut txn).unwrap();
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, "new content");
assert_eq!(txn.modified_count(), 1);
}
#[test]
fn test_apply_with_rollback_rollback_on_error() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path();
let initial_content = "initial content\n";
fs::write(file_path, initial_content).unwrap();
let mut txn = RenameTransaction::new();
let result = apply_with_rollback(
file_path,
|| Err(SpliceError::Other("Simulated failure".to_string())),
&mut txn,
);
assert!(result.is_err());
let content = fs::read_to_string(file_path).unwrap();
assert_eq!(content, initial_content);
}
}