1use anyhow::{Context, Result};
8use chrono::Utc;
9use std::fs;
10use std::path::{Path, PathBuf};
11use tracing::{error, info};
12
13use crate::binary_hash::binary_contains_marker;
14
15#[derive(Debug, Clone)]
23pub struct TestCodeChange {
24 pub file_path: PathBuf,
26 pub original_content: String,
28 pub modified_content: String,
30 pub change_id: String,
32}
33
34impl TestCodeChange {
35 pub fn for_file(file_path: &Path) -> Result<Self> {
52 let original = fs::read_to_string(file_path)
53 .with_context(|| format!("Failed to read source file: {:?}", file_path))?;
54
55 let change_id = format!("RCH_TEST_{}", Utc::now().timestamp_millis());
57
58 let modified = if original.contains("println!(\"Hello, world!\");") {
60 original.replace(
61 "println!(\"Hello, world!\");",
62 &format!("println!(\"Hello, world! {}\");", change_id),
63 )
64 } else if original.contains("println!(\"Hello from test project!\");") {
65 original.replace(
66 "println!(\"Hello from test project!\");",
67 &format!("println!(\"Hello from test project! {}\");", change_id),
68 )
69 } else if original.contains("println!(\"rch self-test\");") {
70 original.replace(
71 "println!(\"rch self-test\");",
72 &format!("println!(\"rch self-test {}\");", change_id),
73 )
74 } else {
75 format!(
77 "{}\n\n// RCH Self-Test Marker (auto-generated, safe to remove)\n\
78 #[unsafe(no_mangle)]\n\
79 #[allow(dead_code)]\n\
80 pub fn {}() -> &'static str {{ \"{}\" }}\n",
81 original, change_id, change_id
82 )
83 };
84
85 Ok(TestCodeChange {
86 file_path: file_path.to_path_buf(),
87 original_content: original,
88 modified_content: modified,
89 change_id,
90 })
91 }
92
93 pub fn for_main_rs(project_dir: &Path) -> Result<Self> {
98 let file_path = project_dir.join("src/main.rs");
99 Self::for_file(&file_path)
100 }
101
102 pub fn for_lib_rs(project_dir: &Path) -> Result<Self> {
107 let file_path = project_dir.join("src/lib.rs");
108 Self::for_file(&file_path)
109 }
110
111 pub fn apply(&self) -> Result<()> {
115 info!(
116 "Applying test change {} to {:?}",
117 self.change_id, self.file_path
118 );
119 fs::write(&self.file_path, &self.modified_content)
120 .with_context(|| format!("Failed to write modified content to {:?}", self.file_path))?;
121 Ok(())
122 }
123
124 pub fn revert(&self) -> Result<()> {
126 info!(
127 "Reverting test change {} from {:?}",
128 self.change_id, self.file_path
129 );
130 fs::write(&self.file_path, &self.original_content).with_context(|| {
131 format!("Failed to restore original content to {:?}", self.file_path)
132 })?;
133 Ok(())
134 }
135
136 pub fn verify_in_binary(&self, binary_path: &Path) -> Result<bool> {
146 binary_contains_marker(binary_path, &self.change_id)
147 }
148}
149
150pub struct TestChangeGuard {
171 change: TestCodeChange,
172 applied: bool,
173}
174
175impl TestChangeGuard {
176 pub fn new(change: TestCodeChange) -> Result<Self> {
180 let mut guard = Self {
181 change,
182 applied: false,
183 };
184 guard.change.apply()?;
185 guard.applied = true;
186 Ok(guard)
187 }
188
189 pub fn change_id(&self) -> &str {
191 &self.change.change_id
192 }
193
194 pub fn file_path(&self) -> &Path {
196 &self.change.file_path
197 }
198
199 pub fn verify_in_binary(&self, binary_path: &Path) -> Result<bool> {
201 self.change.verify_in_binary(binary_path)
202 }
203
204 pub fn revert(mut self) -> Result<()> {
208 if self.applied {
209 self.change.revert()?;
210 self.applied = false;
211 }
212 Ok(())
213 }
214}
215
216impl Drop for TestChangeGuard {
217 fn drop(&mut self) {
218 if self.applied
219 && let Err(e) = self.change.revert()
220 {
221 error!("Failed to revert test change: {}", e);
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use tempfile::TempDir;
230
231 fn init_test_logging() {
232 let _ = tracing_subscriber::fmt()
233 .with_test_writer()
234 .with_max_level(tracing::Level::INFO)
235 .try_init();
236 }
237
238 #[test]
239 fn test_create_test_change() {
240 init_test_logging();
241 info!("TEST START: test_create_test_change");
242
243 let temp_dir = TempDir::new().unwrap();
244 let file_path = temp_dir.path().join("test.rs");
245 let original_content = "fn main() {\n println!(\"Hello\");\n}\n";
246 fs::write(&file_path, original_content).unwrap();
247
248 info!("INPUT: TestCodeChange::for_file({:?})", file_path);
249
250 let change = TestCodeChange::for_file(&file_path).unwrap();
251
252 info!("RESULT: change_id={}", change.change_id);
253 info!(
254 "RESULT: modified_content_len={}",
255 change.modified_content.len()
256 );
257
258 assert!(change.change_id.starts_with("RCH_TEST_"));
259 assert!(change.modified_content.contains(&change.change_id));
260 assert!(change.modified_content.contains("// RCH Self-Test Marker"));
261 assert_eq!(change.original_content, original_content);
262
263 info!("VERIFY: Test change created successfully");
264 info!("TEST PASS: test_create_test_change");
265 }
266
267 #[test]
268 fn test_apply_and_revert() {
269 init_test_logging();
270 info!("TEST START: test_apply_and_revert");
271
272 let temp_dir = TempDir::new().unwrap();
273 let file_path = temp_dir.path().join("test.rs");
274 let original_content = "fn main() {}\n";
275 fs::write(&file_path, original_content).unwrap();
276
277 let change = TestCodeChange::for_file(&file_path).unwrap();
278
279 info!("INPUT: apply then revert test change");
280
281 change.apply().unwrap();
283 let after_apply = fs::read_to_string(&file_path).unwrap();
284 info!(
285 "AFTER APPLY: contains_marker={}",
286 after_apply.contains(&change.change_id)
287 );
288 assert!(after_apply.contains(&change.change_id));
289
290 change.revert().unwrap();
292 let after_revert = fs::read_to_string(&file_path).unwrap();
293 info!(
294 "AFTER REVERT: equals_original={}",
295 after_revert == original_content
296 );
297 assert_eq!(after_revert, original_content);
298
299 info!("VERIFY: Apply and revert work correctly");
300 info!("TEST PASS: test_apply_and_revert");
301 }
302
303 #[test]
304 fn test_guard_auto_reverts() {
305 init_test_logging();
306 info!("TEST START: test_guard_auto_reverts");
307
308 let temp_dir = TempDir::new().unwrap();
309 let file_path = temp_dir.path().join("test.rs");
310 let original_content = "fn main() {}\n";
311 fs::write(&file_path, original_content).unwrap();
312
313 let change_id: String;
314 {
315 let change = TestCodeChange::for_file(&file_path).unwrap();
316 change_id = change.change_id.clone();
317 let _guard = TestChangeGuard::new(change).unwrap();
318
319 let during = fs::read_to_string(&file_path).unwrap();
321 info!(
322 "DURING GUARD: contains_marker={}",
323 during.contains(&change_id)
324 );
325 assert!(during.contains(&change_id));
326
327 }
329
330 let after = fs::read_to_string(&file_path).unwrap();
332 info!("AFTER DROP: equals_original={}", after == original_content);
333 assert_eq!(after, original_content);
334 assert!(!after.contains(&change_id));
335
336 info!("VERIFY: Guard auto-reverts on drop");
337 info!("TEST PASS: test_guard_auto_reverts");
338 }
339
340 #[test]
341 fn test_change_id_unique() {
342 init_test_logging();
343 info!("TEST START: test_change_id_unique");
344
345 let temp_dir = TempDir::new().unwrap();
346 let file_path = temp_dir.path().join("test.rs");
347 fs::write(&file_path, "fn main() {}\n").unwrap();
348
349 let change1 = TestCodeChange::for_file(&file_path).unwrap();
350 std::thread::sleep(std::time::Duration::from_millis(2));
352 let change2 = TestCodeChange::for_file(&file_path).unwrap();
353
354 info!(
355 "RESULT: change1_id={}, change2_id={}",
356 change1.change_id, change2.change_id
357 );
358
359 assert_ne!(change1.change_id, change2.change_id);
360
361 info!("VERIFY: Each change has unique ID");
362 info!("TEST PASS: test_change_id_unique");
363 }
364
365 #[test]
366 fn test_for_main_rs() {
367 init_test_logging();
368 info!("TEST START: test_for_main_rs");
369
370 let temp_dir = TempDir::new().unwrap();
371 let src_dir = temp_dir.path().join("src");
372 fs::create_dir(&src_dir).unwrap();
373 let main_rs = src_dir.join("main.rs");
374 fs::write(&main_rs, "fn main() {}\n").unwrap();
375
376 info!("INPUT: TestCodeChange::for_main_rs({:?})", temp_dir.path());
377
378 let change = TestCodeChange::for_main_rs(temp_dir.path()).unwrap();
379
380 info!("RESULT: file_path={:?}", change.file_path);
381
382 assert_eq!(change.file_path, main_rs);
383
384 info!("VERIFY: for_main_rs finds correct path");
385 info!("TEST PASS: test_for_main_rs");
386 }
387
388 #[test]
389 fn test_nonexistent_file_error() {
390 init_test_logging();
391 info!("TEST START: test_nonexistent_file_error");
392
393 let result = TestCodeChange::for_file(Path::new("/nonexistent/file.rs"));
394
395 info!("RESULT: is_err={}", result.is_err());
396
397 assert!(result.is_err());
398
399 info!("VERIFY: Nonexistent file returns error");
400 info!("TEST PASS: test_nonexistent_file_error");
401 }
402
403 #[test]
404 fn test_change_debug() {
405 init_test_logging();
406 info!("TEST START: test_change_debug");
407
408 let temp_dir = TempDir::new().unwrap();
409 let file_path = temp_dir.path().join("test.rs");
410 fs::write(&file_path, "fn main() {}\n").unwrap();
411
412 let change = TestCodeChange::for_file(&file_path).unwrap();
413 let debug = format!("{:?}", change);
414
415 assert!(debug.contains("TestCodeChange"));
416 assert!(debug.contains("RCH_TEST_"));
417
418 info!("TEST PASS: test_change_debug");
419 }
420
421 #[test]
422 fn test_change_clone() {
423 init_test_logging();
424 info!("TEST START: test_change_clone");
425
426 let temp_dir = TempDir::new().unwrap();
427 let file_path = temp_dir.path().join("test.rs");
428 fs::write(&file_path, "fn main() {}\n").unwrap();
429
430 let change = TestCodeChange::for_file(&file_path).unwrap();
431 let cloned = change.clone();
432
433 assert_eq!(change.change_id, cloned.change_id);
434 assert_eq!(change.file_path, cloned.file_path);
435 assert_eq!(change.original_content, cloned.original_content);
436 assert_eq!(change.modified_content, cloned.modified_content);
437
438 info!("TEST PASS: test_change_clone");
439 }
440
441 #[test]
442 fn test_for_lib_rs() {
443 init_test_logging();
444 info!("TEST START: test_for_lib_rs");
445
446 let temp_dir = TempDir::new().unwrap();
447 let src_dir = temp_dir.path().join("src");
448 fs::create_dir(&src_dir).unwrap();
449 let lib_rs = src_dir.join("lib.rs");
450 fs::write(&lib_rs, "pub fn hello() {}\n").unwrap();
451
452 let change = TestCodeChange::for_lib_rs(temp_dir.path()).unwrap();
453
454 assert_eq!(change.file_path, lib_rs);
455 assert!(change.change_id.starts_with("RCH_TEST_"));
456
457 info!("TEST PASS: test_for_lib_rs");
458 }
459
460 #[test]
461 fn test_for_lib_rs_nonexistent() {
462 init_test_logging();
463 info!("TEST START: test_for_lib_rs_nonexistent");
464
465 let temp_dir = TempDir::new().unwrap();
466 let result = TestCodeChange::for_lib_rs(temp_dir.path());
469 assert!(result.is_err());
470
471 info!("TEST PASS: test_for_lib_rs_nonexistent");
472 }
473
474 #[test]
475 fn test_change_with_hello_world_pattern() {
476 init_test_logging();
477 info!("TEST START: test_change_with_hello_world_pattern");
478
479 let temp_dir = TempDir::new().unwrap();
480 let file_path = temp_dir.path().join("main.rs");
481 let original = r#"fn main() {
482 println!("Hello, world!");
483}"#;
484 fs::write(&file_path, original).unwrap();
485
486 let change = TestCodeChange::for_file(&file_path).unwrap();
487
488 assert!(change.modified_content.contains("Hello, world!"));
490 assert!(change.modified_content.contains(&change.change_id));
491 assert!(!change.modified_content.contains("// RCH Self-Test Marker"));
492
493 info!("TEST PASS: test_change_with_hello_world_pattern");
494 }
495
496 #[test]
497 fn test_change_with_hello_from_test_project_pattern() {
498 init_test_logging();
499 info!("TEST START: test_change_with_hello_from_test_project_pattern");
500
501 let temp_dir = TempDir::new().unwrap();
502 let file_path = temp_dir.path().join("main.rs");
503 let original = r#"fn main() {
504 println!("Hello from test project!");
505}"#;
506 fs::write(&file_path, original).unwrap();
507
508 let change = TestCodeChange::for_file(&file_path).unwrap();
509
510 assert!(change.modified_content.contains("Hello from test project!"));
512 assert!(change.modified_content.contains(&change.change_id));
513 assert!(!change.modified_content.contains("// RCH Self-Test Marker"));
514
515 info!("TEST PASS: test_change_with_hello_from_test_project_pattern");
516 }
517
518 #[test]
519 fn test_guard_change_id() {
520 init_test_logging();
521 info!("TEST START: test_guard_change_id");
522
523 let temp_dir = TempDir::new().unwrap();
524 let file_path = temp_dir.path().join("test.rs");
525 fs::write(&file_path, "fn main() {}\n").unwrap();
526
527 let change = TestCodeChange::for_file(&file_path).unwrap();
528 let expected_id = change.change_id.clone();
529 let guard = TestChangeGuard::new(change).unwrap();
530
531 assert_eq!(guard.change_id(), expected_id);
532
533 info!("TEST PASS: test_guard_change_id");
534 }
535
536 #[test]
537 fn test_guard_file_path() {
538 init_test_logging();
539 info!("TEST START: test_guard_file_path");
540
541 let temp_dir = TempDir::new().unwrap();
542 let file_path = temp_dir.path().join("test.rs");
543 fs::write(&file_path, "fn main() {}\n").unwrap();
544
545 let change = TestCodeChange::for_file(&file_path).unwrap();
546 let guard = TestChangeGuard::new(change).unwrap();
547
548 assert_eq!(guard.file_path(), file_path);
549
550 info!("TEST PASS: test_guard_file_path");
551 }
552
553 #[test]
554 fn test_guard_manual_revert() {
555 init_test_logging();
556 info!("TEST START: test_guard_manual_revert");
557
558 let temp_dir = TempDir::new().unwrap();
559 let file_path = temp_dir.path().join("test.rs");
560 let original_content = "fn main() {}\n";
561 fs::write(&file_path, original_content).unwrap();
562
563 let change = TestCodeChange::for_file(&file_path).unwrap();
564 let guard = TestChangeGuard::new(change).unwrap();
565
566 let during = fs::read_to_string(&file_path).unwrap();
568 assert!(during.contains("RCH_TEST_"));
569
570 guard.revert().unwrap();
572
573 let after = fs::read_to_string(&file_path).unwrap();
575 assert_eq!(after, original_content);
576
577 info!("TEST PASS: test_guard_manual_revert");
578 }
579
580 #[test]
581 fn test_change_with_empty_file() {
582 init_test_logging();
583 info!("TEST START: test_change_with_empty_file");
584
585 let temp_dir = TempDir::new().unwrap();
586 let file_path = temp_dir.path().join("empty.rs");
587 fs::write(&file_path, "").unwrap();
588
589 let change = TestCodeChange::for_file(&file_path).unwrap();
590
591 assert!(change.modified_content.contains("// RCH Self-Test Marker"));
593 assert!(change.modified_content.contains(&change.change_id));
594 assert!(change.original_content.is_empty());
595
596 info!("TEST PASS: test_change_with_empty_file");
597 }
598
599 #[test]
600 fn test_multiple_apply_same_change() {
601 init_test_logging();
602 info!("TEST START: test_multiple_apply_same_change");
603
604 let temp_dir = TempDir::new().unwrap();
605 let file_path = temp_dir.path().join("test.rs");
606 fs::write(&file_path, "fn main() {}\n").unwrap();
607
608 let change = TestCodeChange::for_file(&file_path).unwrap();
609
610 change.apply().unwrap();
612 change.apply().unwrap();
613
614 let content = fs::read_to_string(&file_path).unwrap();
615 assert!(content.contains(&change.change_id));
616
617 info!("TEST PASS: test_multiple_apply_same_change");
618 }
619
620 #[test]
621 fn test_apply_revert_apply() {
622 init_test_logging();
623 info!("TEST START: test_apply_revert_apply");
624
625 let temp_dir = TempDir::new().unwrap();
626 let file_path = temp_dir.path().join("test.rs");
627 let original = "fn main() {}\n";
628 fs::write(&file_path, original).unwrap();
629
630 let change = TestCodeChange::for_file(&file_path).unwrap();
631
632 change.apply().unwrap();
634 assert!(
635 fs::read_to_string(&file_path)
636 .unwrap()
637 .contains(&change.change_id)
638 );
639
640 change.revert().unwrap();
642 assert_eq!(fs::read_to_string(&file_path).unwrap(), original);
643
644 change.apply().unwrap();
646 assert!(
647 fs::read_to_string(&file_path)
648 .unwrap()
649 .contains(&change.change_id)
650 );
651
652 info!("TEST PASS: test_apply_revert_apply");
653 }
654
655 #[test]
656 fn test_guard_preserves_change() {
657 init_test_logging();
658 info!("TEST START: test_guard_preserves_change");
659
660 let temp_dir = TempDir::new().unwrap();
661 let file_path = temp_dir.path().join("test.rs");
662 fs::write(&file_path, "fn main() {}\n").unwrap();
663
664 let change = TestCodeChange::for_file(&file_path).unwrap();
665 let original_content = change.original_content.clone();
666 let modified_content = change.modified_content.clone();
667
668 let guard = TestChangeGuard::new(change).unwrap();
669
670 assert!(modified_content.contains(guard.change_id()));
672
673 drop(guard);
674
675 let after = fs::read_to_string(&file_path).unwrap();
677 assert_eq!(after, original_content);
678
679 info!("TEST PASS: test_guard_preserves_change");
680 }
681
682 #[test]
683 fn test_change_id_format() {
684 init_test_logging();
685 info!("TEST START: test_change_id_format");
686
687 let temp_dir = TempDir::new().unwrap();
688 let file_path = temp_dir.path().join("test.rs");
689 fs::write(&file_path, "fn main() {}\n").unwrap();
690
691 let change = TestCodeChange::for_file(&file_path).unwrap();
692
693 assert!(change.change_id.starts_with("RCH_TEST_"));
695 let timestamp_part = &change.change_id["RCH_TEST_".len()..];
696 assert!(timestamp_part.parse::<i64>().is_ok());
697
698 info!("TEST PASS: test_change_id_format");
699 }
700}