use anyhow::{Context, Result};
use chrono::Utc;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{error, info};
use crate::binary_hash::binary_contains_marker;
#[derive(Debug, Clone)]
pub struct TestCodeChange {
pub file_path: PathBuf,
pub original_content: String,
pub modified_content: String,
pub change_id: String,
}
impl TestCodeChange {
pub fn for_file(file_path: &Path) -> Result<Self> {
let original = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read source file: {:?}", file_path))?;
let change_id = format!("RCH_TEST_{}", Utc::now().timestamp_millis());
let modified = if original.contains("println!(\"Hello, world!\");") {
original.replace(
"println!(\"Hello, world!\");",
&format!("println!(\"Hello, world! {}\");", change_id),
)
} else if original.contains("println!(\"Hello from test project!\");") {
original.replace(
"println!(\"Hello from test project!\");",
&format!("println!(\"Hello from test project! {}\");", change_id),
)
} else if original.contains("println!(\"rch self-test\");") {
original.replace(
"println!(\"rch self-test\");",
&format!("println!(\"rch self-test {}\");", change_id),
)
} else {
format!(
"{}\n\n// RCH Self-Test Marker (auto-generated, safe to remove)\n\
#[unsafe(no_mangle)]\n\
#[allow(dead_code)]\n\
pub fn {}() -> &'static str {{ \"{}\" }}\n",
original, change_id, change_id
)
};
Ok(TestCodeChange {
file_path: file_path.to_path_buf(),
original_content: original,
modified_content: modified,
change_id,
})
}
pub fn for_main_rs(project_dir: &Path) -> Result<Self> {
let file_path = project_dir.join("src/main.rs");
Self::for_file(&file_path)
}
pub fn for_lib_rs(project_dir: &Path) -> Result<Self> {
let file_path = project_dir.join("src/lib.rs");
Self::for_file(&file_path)
}
pub fn apply(&self) -> Result<()> {
info!(
"Applying test change {} to {:?}",
self.change_id, self.file_path
);
fs::write(&self.file_path, &self.modified_content)
.with_context(|| format!("Failed to write modified content to {:?}", self.file_path))?;
Ok(())
}
pub fn revert(&self) -> Result<()> {
info!(
"Reverting test change {} from {:?}",
self.change_id, self.file_path
);
fs::write(&self.file_path, &self.original_content).with_context(|| {
format!("Failed to restore original content to {:?}", self.file_path)
})?;
Ok(())
}
pub fn verify_in_binary(&self, binary_path: &Path) -> Result<bool> {
binary_contains_marker(binary_path, &self.change_id)
}
}
pub struct TestChangeGuard {
change: TestCodeChange,
applied: bool,
}
impl TestChangeGuard {
pub fn new(change: TestCodeChange) -> Result<Self> {
let mut guard = Self {
change,
applied: false,
};
guard.change.apply()?;
guard.applied = true;
Ok(guard)
}
pub fn change_id(&self) -> &str {
&self.change.change_id
}
pub fn file_path(&self) -> &Path {
&self.change.file_path
}
pub fn verify_in_binary(&self, binary_path: &Path) -> Result<bool> {
self.change.verify_in_binary(binary_path)
}
pub fn revert(mut self) -> Result<()> {
if self.applied {
self.change.revert()?;
self.applied = false;
}
Ok(())
}
}
impl Drop for TestChangeGuard {
fn drop(&mut self) {
if self.applied
&& let Err(e) = self.change.revert()
{
error!("Failed to revert test change: {}", e);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn init_test_logging() {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_max_level(tracing::Level::INFO)
.try_init();
}
#[test]
fn test_create_test_change() {
init_test_logging();
info!("TEST START: test_create_test_change");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let original_content = "fn main() {\n println!(\"Hello\");\n}\n";
fs::write(&file_path, original_content).unwrap();
info!("INPUT: TestCodeChange::for_file({:?})", file_path);
let change = TestCodeChange::for_file(&file_path).unwrap();
info!("RESULT: change_id={}", change.change_id);
info!(
"RESULT: modified_content_len={}",
change.modified_content.len()
);
assert!(change.change_id.starts_with("RCH_TEST_"));
assert!(change.modified_content.contains(&change.change_id));
assert!(change.modified_content.contains("// RCH Self-Test Marker"));
assert_eq!(change.original_content, original_content);
info!("VERIFY: Test change created successfully");
info!("TEST PASS: test_create_test_change");
}
#[test]
fn test_apply_and_revert() {
init_test_logging();
info!("TEST START: test_apply_and_revert");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let original_content = "fn main() {}\n";
fs::write(&file_path, original_content).unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
info!("INPUT: apply then revert test change");
change.apply().unwrap();
let after_apply = fs::read_to_string(&file_path).unwrap();
info!(
"AFTER APPLY: contains_marker={}",
after_apply.contains(&change.change_id)
);
assert!(after_apply.contains(&change.change_id));
change.revert().unwrap();
let after_revert = fs::read_to_string(&file_path).unwrap();
info!(
"AFTER REVERT: equals_original={}",
after_revert == original_content
);
assert_eq!(after_revert, original_content);
info!("VERIFY: Apply and revert work correctly");
info!("TEST PASS: test_apply_and_revert");
}
#[test]
fn test_guard_auto_reverts() {
init_test_logging();
info!("TEST START: test_guard_auto_reverts");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let original_content = "fn main() {}\n";
fs::write(&file_path, original_content).unwrap();
let change_id: String;
{
let change = TestCodeChange::for_file(&file_path).unwrap();
change_id = change.change_id.clone();
let _guard = TestChangeGuard::new(change).unwrap();
let during = fs::read_to_string(&file_path).unwrap();
info!(
"DURING GUARD: contains_marker={}",
during.contains(&change_id)
);
assert!(during.contains(&change_id));
}
let after = fs::read_to_string(&file_path).unwrap();
info!("AFTER DROP: equals_original={}", after == original_content);
assert_eq!(after, original_content);
assert!(!after.contains(&change_id));
info!("VERIFY: Guard auto-reverts on drop");
info!("TEST PASS: test_guard_auto_reverts");
}
#[test]
fn test_change_id_unique() {
init_test_logging();
info!("TEST START: test_change_id_unique");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change1 = TestCodeChange::for_file(&file_path).unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
let change2 = TestCodeChange::for_file(&file_path).unwrap();
info!(
"RESULT: change1_id={}, change2_id={}",
change1.change_id, change2.change_id
);
assert_ne!(change1.change_id, change2.change_id);
info!("VERIFY: Each change has unique ID");
info!("TEST PASS: test_change_id_unique");
}
#[test]
fn test_for_main_rs() {
init_test_logging();
info!("TEST START: test_for_main_rs");
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir).unwrap();
let main_rs = src_dir.join("main.rs");
fs::write(&main_rs, "fn main() {}\n").unwrap();
info!("INPUT: TestCodeChange::for_main_rs({:?})", temp_dir.path());
let change = TestCodeChange::for_main_rs(temp_dir.path()).unwrap();
info!("RESULT: file_path={:?}", change.file_path);
assert_eq!(change.file_path, main_rs);
info!("VERIFY: for_main_rs finds correct path");
info!("TEST PASS: test_for_main_rs");
}
#[test]
fn test_nonexistent_file_error() {
init_test_logging();
info!("TEST START: test_nonexistent_file_error");
let result = TestCodeChange::for_file(Path::new("/nonexistent/file.rs"));
info!("RESULT: is_err={}", result.is_err());
assert!(result.is_err());
info!("VERIFY: Nonexistent file returns error");
info!("TEST PASS: test_nonexistent_file_error");
}
#[test]
fn test_change_debug() {
init_test_logging();
info!("TEST START: test_change_debug");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let debug = format!("{:?}", change);
assert!(debug.contains("TestCodeChange"));
assert!(debug.contains("RCH_TEST_"));
info!("TEST PASS: test_change_debug");
}
#[test]
fn test_change_clone() {
init_test_logging();
info!("TEST START: test_change_clone");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let cloned = change.clone();
assert_eq!(change.change_id, cloned.change_id);
assert_eq!(change.file_path, cloned.file_path);
assert_eq!(change.original_content, cloned.original_content);
assert_eq!(change.modified_content, cloned.modified_content);
info!("TEST PASS: test_change_clone");
}
#[test]
fn test_for_lib_rs() {
init_test_logging();
info!("TEST START: test_for_lib_rs");
let temp_dir = TempDir::new().unwrap();
let src_dir = temp_dir.path().join("src");
fs::create_dir(&src_dir).unwrap();
let lib_rs = src_dir.join("lib.rs");
fs::write(&lib_rs, "pub fn hello() {}\n").unwrap();
let change = TestCodeChange::for_lib_rs(temp_dir.path()).unwrap();
assert_eq!(change.file_path, lib_rs);
assert!(change.change_id.starts_with("RCH_TEST_"));
info!("TEST PASS: test_for_lib_rs");
}
#[test]
fn test_for_lib_rs_nonexistent() {
init_test_logging();
info!("TEST START: test_for_lib_rs_nonexistent");
let temp_dir = TempDir::new().unwrap();
let result = TestCodeChange::for_lib_rs(temp_dir.path());
assert!(result.is_err());
info!("TEST PASS: test_for_lib_rs_nonexistent");
}
#[test]
fn test_change_with_hello_world_pattern() {
init_test_logging();
info!("TEST START: test_change_with_hello_world_pattern");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("main.rs");
let original = r#"fn main() {
println!("Hello, world!");
}"#;
fs::write(&file_path, original).unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
assert!(change.modified_content.contains("Hello, world!"));
assert!(change.modified_content.contains(&change.change_id));
assert!(!change.modified_content.contains("// RCH Self-Test Marker"));
info!("TEST PASS: test_change_with_hello_world_pattern");
}
#[test]
fn test_change_with_hello_from_test_project_pattern() {
init_test_logging();
info!("TEST START: test_change_with_hello_from_test_project_pattern");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("main.rs");
let original = r#"fn main() {
println!("Hello from test project!");
}"#;
fs::write(&file_path, original).unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
assert!(change.modified_content.contains("Hello from test project!"));
assert!(change.modified_content.contains(&change.change_id));
assert!(!change.modified_content.contains("// RCH Self-Test Marker"));
info!("TEST PASS: test_change_with_hello_from_test_project_pattern");
}
#[test]
fn test_guard_change_id() {
init_test_logging();
info!("TEST START: test_guard_change_id");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let expected_id = change.change_id.clone();
let guard = TestChangeGuard::new(change).unwrap();
assert_eq!(guard.change_id(), expected_id);
info!("TEST PASS: test_guard_change_id");
}
#[test]
fn test_guard_file_path() {
init_test_logging();
info!("TEST START: test_guard_file_path");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let guard = TestChangeGuard::new(change).unwrap();
assert_eq!(guard.file_path(), file_path);
info!("TEST PASS: test_guard_file_path");
}
#[test]
fn test_guard_manual_revert() {
init_test_logging();
info!("TEST START: test_guard_manual_revert");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let original_content = "fn main() {}\n";
fs::write(&file_path, original_content).unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let guard = TestChangeGuard::new(change).unwrap();
let during = fs::read_to_string(&file_path).unwrap();
assert!(during.contains("RCH_TEST_"));
guard.revert().unwrap();
let after = fs::read_to_string(&file_path).unwrap();
assert_eq!(after, original_content);
info!("TEST PASS: test_guard_manual_revert");
}
#[test]
fn test_change_with_empty_file() {
init_test_logging();
info!("TEST START: test_change_with_empty_file");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("empty.rs");
fs::write(&file_path, "").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
assert!(change.modified_content.contains("// RCH Self-Test Marker"));
assert!(change.modified_content.contains(&change.change_id));
assert!(change.original_content.is_empty());
info!("TEST PASS: test_change_with_empty_file");
}
#[test]
fn test_multiple_apply_same_change() {
init_test_logging();
info!("TEST START: test_multiple_apply_same_change");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
change.apply().unwrap();
change.apply().unwrap();
let content = fs::read_to_string(&file_path).unwrap();
assert!(content.contains(&change.change_id));
info!("TEST PASS: test_multiple_apply_same_change");
}
#[test]
fn test_apply_revert_apply() {
init_test_logging();
info!("TEST START: test_apply_revert_apply");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
let original = "fn main() {}\n";
fs::write(&file_path, original).unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
change.apply().unwrap();
assert!(
fs::read_to_string(&file_path)
.unwrap()
.contains(&change.change_id)
);
change.revert().unwrap();
assert_eq!(fs::read_to_string(&file_path).unwrap(), original);
change.apply().unwrap();
assert!(
fs::read_to_string(&file_path)
.unwrap()
.contains(&change.change_id)
);
info!("TEST PASS: test_apply_revert_apply");
}
#[test]
fn test_guard_preserves_change() {
init_test_logging();
info!("TEST START: test_guard_preserves_change");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
let original_content = change.original_content.clone();
let modified_content = change.modified_content.clone();
let guard = TestChangeGuard::new(change).unwrap();
assert!(modified_content.contains(guard.change_id()));
drop(guard);
let after = fs::read_to_string(&file_path).unwrap();
assert_eq!(after, original_content);
info!("TEST PASS: test_guard_preserves_change");
}
#[test]
fn test_change_id_format() {
init_test_logging();
info!("TEST START: test_change_id_format");
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(&file_path, "fn main() {}\n").unwrap();
let change = TestCodeChange::for_file(&file_path).unwrap();
assert!(change.change_id.starts_with("RCH_TEST_"));
let timestamp_part = &change.change_id["RCH_TEST_".len()..];
assert!(timestamp_part.parse::<i64>().is_ok());
info!("TEST PASS: test_change_id_format");
}
}