1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#![cfg_attr(coverage_nightly, coverage(off))]
//! RAII-based mutant file restoration guard
//!
//! Provides a safe file backup and restoration mechanism for mutation testing,
//! ensuring that original files are always restored even on error or process
//! termination.
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use tokio::fs;
/// RAII guard that ensures a file is restored on scope exit
///
/// Uses a combination of async operations for normal execution
/// and sync operations in Drop to ensure restoration even on panic.
pub struct MutantGuard {
/// Original file path that was modified
original_path: PathBuf,
/// Path to the backup file
backup_path: PathBuf,
/// Whether backup has already been restored
restored: bool,
}
impl MutantGuard {
/// Create a new guard by backing up a file
///
/// # Arguments
///
/// * `path` - Path to the file that will be modified
///
/// # Returns
///
/// A new guard instance that will restore the file when dropped
///
/// # Errors
///
/// Returns an error if the backup cannot be created
pub async fn new(path: &Path) -> Result<Self> {
// Create backup path with a unique suffix
let backup_path = path.with_extension(format!("pmat_backup_{}", std::process::id()));
// Create backup of the original file
fs::copy(path, &backup_path)
.await
.with_context(|| format!("Failed to create backup of {}", path.display()))?;
Ok(Self {
original_path: path.to_path_buf(),
backup_path,
restored: false,
})
}
/// Restore the file from backup
///
/// # Returns
///
/// Ok(()) if the restoration was successful, or an error otherwise
///
/// # Errors
///
/// Returns an error if the restoration fails
pub async fn restore(&mut self) -> Result<()> {
if self.restored {
return Ok(());
}
// Copy backup back to original file
fs::copy(&self.backup_path, &self.original_path)
.await
.with_context(|| "Failed to restore file from backup".to_string())?;
// Remove backup file
fs::remove_file(&self.backup_path)
.await
.with_context(|| "Failed to remove backup file".to_string())?;
self.restored = true;
Ok(())
}
/// Mark this guard as restored without actually restoring
///
/// This is useful when another mechanism has already restored the file
/// and we don't want the Drop handler to attempt restoration again.
pub fn mark_restored(&mut self) {
self.restored = true;
}
/// Get the path to the backup file
pub fn backup_path(&self) -> &Path {
&self.backup_path
}
/// Get the path to the original file
pub fn original_path(&self) -> &Path {
&self.original_path
}
}
impl Drop for MutantGuard {
fn drop(&mut self) {
// Skip if already restored
if self.restored {
return;
}
// Use blocking operations to ensure files are restored
// even in the case of tokio runtime shutdown
if let Err(e) = std::fs::copy(&self.backup_path, &self.original_path) {
eprintln!("ERROR: Failed to restore file in MutantGuard::drop: {}", e);
}
if let Err(e) = std::fs::remove_file(&self.backup_path) {
eprintln!(
"ERROR: Failed to remove backup file in MutantGuard::drop: {}",
e
);
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::NamedTempFile;
// No external imports needed
#[tokio::test]
async fn test_guard_creation_and_restoration() -> Result<()> {
// Create a temp file
let temp_file = NamedTempFile::new()?;
let temp_path = temp_file.path().to_owned();
// Write initial content
fs::write(&temp_path, "initial content")?;
// Create guard
let mut guard = MutantGuard::new(&temp_path).await?;
// Verify backup exists
assert!(guard.backup_path().exists());
assert_eq!(fs::read_to_string(guard.backup_path())?, "initial content");
// Modify the file
fs::write(&temp_path, "modified content")?;
assert_eq!(fs::read_to_string(&temp_path)?, "modified content");
// Restore the file
guard.restore().await?;
// Verify content was restored
assert_eq!(fs::read_to_string(&temp_path)?, "initial content");
// Verify backup was deleted
assert!(!guard.backup_path().exists());
Ok(())
}
#[tokio::test]
async fn test_guard_drop_restores_file() -> Result<()> {
// Create a temp file
let temp_file = NamedTempFile::new()?;
let temp_path = temp_file.path().to_owned();
// Write initial content
fs::write(&temp_path, "initial content")?;
// Use a block to force drop
{
// Create guard
let _guard = MutantGuard::new(&temp_path).await?;
// Modify the file
fs::write(&temp_path, "modified by test")?;
assert_eq!(fs::read_to_string(&temp_path)?, "modified by test");
// Guard is dropped here
}
// Verify content was restored by Drop
assert_eq!(fs::read_to_string(&temp_path)?, "initial content");
Ok(())
}
#[tokio::test]
async fn test_mark_restored_prevents_restoration() -> Result<()> {
// Create a temp file
let temp_file = NamedTempFile::new()?;
let temp_path = temp_file.path().to_owned();
// Write initial content
fs::write(&temp_path, "initial content")?;
// Create guard
let mut guard = MutantGuard::new(&temp_path).await?;
// Modify the file
fs::write(&temp_path, "modified by test")?;
// Mark as restored (without actually restoring)
guard.mark_restored();
// Guard is dropped here, but shouldn't restore
drop(guard);
// Verify content was NOT restored
assert_eq!(fs::read_to_string(&temp_path)?, "modified by test");
Ok(())
}
}