1use super::{TxAction, TxEntry, TxLog};
44use crate::storage::{InMemoryStateStore, StateRef, StateStore};
45use ryo_mutations::SerializableMutation;
46use ryo_source::pure::PureFile;
47use std::path::Path;
48use thiserror::Error;
49
50#[derive(Debug, Error)]
52pub enum ReplayError {
53 #[error("Mutation data missing for entry #{0}")]
56 MissingMutationData(u64),
57
58 #[error("Failed to parse mutation data: {0}")]
60 ParseError(String),
61
62 #[error("Mutation type '{0}' cannot be replayed (Generic mutation)")]
65 UnreplayableMutation(String),
66
67 #[error("Pre-state mismatch at entry #{id}: expected {expected}, got {actual}")]
69 PreStateMismatch {
70 id: u64,
72 expected: String,
74 actual: String,
76 },
77
78 #[error("Post-state mismatch at entry #{id}: expected {expected}, got {actual}")]
80 PostStateMismatch {
81 id: u64,
83 expected: String,
85 actual: String,
87 },
88
89 #[error("Mutation failed: {0}")]
91 MutationFailed(String),
92}
93
94#[derive(Debug, Clone)]
96pub struct ReplayResult {
97 pub mutations_applied: usize,
99 pub total_changes: usize,
101 pub skipped: usize,
103 pub warnings: Vec<String>,
105}
106
107impl ReplayResult {
108 fn new() -> Self {
109 Self {
110 mutations_applied: 0,
111 total_changes: 0,
112 skipped: 0,
113 warnings: Vec::new(),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, Default)]
120pub enum ReplayMode {
121 #[default]
123 Fast,
124 VerifyPre,
126 VerifyPost,
128 VerifyBoth,
130}
131
132impl ReplayMode {
133 pub fn verify_pre(&self) -> bool {
136 matches!(self, ReplayMode::VerifyPre | ReplayMode::VerifyBoth)
137 }
138
139 pub fn verify_post(&self) -> bool {
142 matches!(self, ReplayMode::VerifyPost | ReplayMode::VerifyBoth)
143 }
144}
145
146pub struct ReplayEngine<'a> {
148 log: &'a TxLog,
149 mode: ReplayMode,
150 state_store: InMemoryStateStore,
151}
152
153impl<'a> ReplayEngine<'a> {
154 pub fn new(log: &'a TxLog) -> Self {
156 Self {
157 log,
158 mode: ReplayMode::default(),
159 state_store: InMemoryStateStore::new(),
160 }
161 }
162
163 pub fn with_mode(mut self, mode: ReplayMode) -> Self {
165 self.mode = mode;
166 self
167 }
168
169 pub fn replay_all(&mut self, file: &mut PureFile) -> Result<ReplayResult, ReplayError> {
173 let mut result = ReplayResult::new();
174
175 for entry in self.log.entries() {
176 match &entry.action {
177 TxAction::MutationApplied {
178 mutation_type,
179 mutation_data,
180 pre_state,
181 post_state,
182 ..
183 } => {
184 match self.replay_mutation_entry(
186 entry,
187 mutation_type,
188 mutation_data,
189 pre_state.as_ref(),
190 post_state.as_ref(),
191 file,
192 ) {
193 Ok(changes) => {
194 result.mutations_applied += 1;
195 result.total_changes += changes;
196 }
197 Err(ReplayError::UnreplayableMutation(t)) => {
198 result.skipped += 1;
199 result.warnings.push(format!(
200 "Skipped unreplayable mutation: {} (entry #{})",
201 t, entry.id
202 ));
203 }
204 Err(ReplayError::MissingMutationData(_)) => {
205 result.skipped += 1;
206 result.warnings.push(format!(
207 "Skipped mutation without data (entry #{})",
208 entry.id
209 ));
210 }
211 Err(e) => return Err(e),
212 }
213 }
214 _ => {
215 }
217 }
218 }
219
220 Ok(result)
221 }
222
223 pub fn replay_file(
227 &mut self,
228 target_path: &Path,
229 file: &mut PureFile,
230 ) -> Result<ReplayResult, ReplayError> {
231 let mut result = ReplayResult::new();
232
233 for entry in self.log.entries() {
234 if let TxAction::MutationApplied {
235 mutation_type,
236 mutation_data,
237 file_path,
238 pre_state,
239 post_state,
240 ..
241 } = &entry.action
242 {
243 let matches = file_path.as_ref().map(|p| p == target_path).unwrap_or(true); if !matches {
247 continue;
248 }
249
250 match self.replay_mutation_entry(
251 entry,
252 mutation_type,
253 mutation_data,
254 pre_state.as_ref(),
255 post_state.as_ref(),
256 file,
257 ) {
258 Ok(changes) => {
259 result.mutations_applied += 1;
260 result.total_changes += changes;
261 }
262 Err(ReplayError::UnreplayableMutation(t)) => {
263 result.skipped += 1;
264 result.warnings.push(format!(
265 "Skipped unreplayable mutation: {} (entry #{})",
266 t, entry.id
267 ));
268 }
269 Err(ReplayError::MissingMutationData(_)) => {
270 result.skipped += 1;
271 result.warnings.push(format!(
272 "Skipped mutation without data (entry #{})",
273 entry.id
274 ));
275 }
276 Err(e) => return Err(e),
277 }
278 }
279 }
280
281 Ok(result)
282 }
283
284 fn replay_mutation_entry(
286 &mut self,
287 entry: &TxEntry,
288 mutation_type: &str,
289 mutation_data: &Option<serde_json::Value>,
290 pre_state: Option<&StateRef>,
291 post_state: Option<&StateRef>,
292 file: &mut PureFile,
293 ) -> Result<usize, ReplayError> {
294 if self.mode.verify_pre() {
296 if let Some(expected_pre) = pre_state {
297 let current = self.state_store.store(
298 &file
299 .to_source()
300 .map_err(|e| ReplayError::MutationFailed(e.to_string()))?,
301 );
302 if ¤t != expected_pre {
303 return Err(ReplayError::PreStateMismatch {
304 id: entry.id,
305 expected: expected_pre.short().to_string(),
306 actual: current.short().to_string(),
307 });
308 }
309 }
310 }
311
312 let data = mutation_data
314 .as_ref()
315 .ok_or(ReplayError::MissingMutationData(entry.id))?;
316
317 let serializable: SerializableMutation = serde_json::from_value(data.clone())
319 .map_err(|e| ReplayError::ParseError(e.to_string()))?;
320
321 let mutation = serializable
323 .to_mutation()
324 .ok_or_else(|| ReplayError::UnreplayableMutation(mutation_type.to_string()))?;
325
326 let _mutation = mutation; let changes = 0usize; if self.mode.verify_post() {
335 if let Some(expected_post) = post_state {
336 let current = self.state_store.store(
337 &file
338 .to_source()
339 .map_err(|e| ReplayError::MutationFailed(e.to_string()))?,
340 );
341 if ¤t != expected_post {
342 return Err(ReplayError::PostStateMismatch {
343 id: entry.id,
344 expected: expected_post.short().to_string(),
345 actual: current.short().to_string(),
346 });
347 }
348 }
349 }
350
351 Ok(changes)
352 }
353
354 pub fn analyze(&self) -> ReplayAnalysis {
356 let mut analysis = ReplayAnalysis::default();
357
358 for entry in self.log.entries() {
359 if let TxAction::MutationApplied {
360 mutation_type,
361 mutation_data,
362 pre_state,
363 post_state,
364 ..
365 } = &entry.action
366 {
367 analysis.total_mutations += 1;
368
369 if mutation_data.is_none() {
371 analysis.missing_data += 1;
372 analysis
373 .unreplayable_types
374 .push(format!("{} (entry #{}, no data)", mutation_type, entry.id));
375 continue;
376 }
377
378 if pre_state.is_some() {
380 analysis.with_pre_state += 1;
381 }
382 if post_state.is_some() {
383 analysis.with_post_state += 1;
384 }
385
386 if let Some(data) = mutation_data {
388 match serde_json::from_value::<SerializableMutation>(data.clone()) {
389 Ok(sm) => {
390 if sm.to_mutation().is_some() {
391 analysis.replayable += 1;
392 } else {
393 analysis.unreplayable_types.push(format!(
394 "{} (entry #{}, Generic)",
395 mutation_type, entry.id
396 ));
397 }
398 }
399 Err(_) => {
400 analysis.parse_errors += 1;
401 analysis.unreplayable_types.push(format!(
402 "{} (entry #{}, parse error)",
403 mutation_type, entry.id
404 ));
405 }
406 }
407 }
408 }
409 }
410
411 analysis
412 }
413}
414
415#[derive(Debug, Default)]
417pub struct ReplayAnalysis {
418 pub total_mutations: usize,
420 pub replayable: usize,
422 pub missing_data: usize,
424 pub parse_errors: usize,
426 pub with_pre_state: usize,
428 pub with_post_state: usize,
430 pub unreplayable_types: Vec<String>,
432}
433
434impl ReplayAnalysis {
435 pub fn replayable_percentage(&self) -> f64 {
437 if self.total_mutations == 0 {
438 100.0
439 } else {
440 (self.replayable as f64 / self.total_mutations as f64) * 100.0
441 }
442 }
443
444 pub fn is_fully_replayable(&self) -> bool {
446 self.replayable == self.total_mutations
447 }
448
449 pub fn can_verify(&self) -> bool {
451 self.with_pre_state == self.total_mutations && self.with_post_state == self.total_mutations
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::txlog::TxAction;
459 use ryo_analysis::{SymbolId, SymbolKind, SymbolPath, SymbolRegistry};
460 use ryo_mutations::RenameMutation;
461
462 fn dummy_symbol_id() -> SymbolId {
464 let mut registry = SymbolRegistry::new();
465 let path = SymbolPath::parse("test_crate::test_dummy").unwrap();
466 registry.register(path, SymbolKind::Function).unwrap()
467 }
468
469 fn create_test_log_with_mutation() -> TxLog {
470 let mut log = TxLog::new();
471
472 let mutation = RenameMutation {
474 symbol_id: dummy_symbol_id(),
475 to: "bar".to_string(),
476 };
477 use ryo_mutations::ToSerializable;
478 let serializable = mutation.to_serializable();
479
480 log.log(TxAction::MutationApplied {
481 mutation_type: "Rename".to_string(),
482 target: "foo -> bar".to_string(),
483 changes: 1,
484 mutation_data: Some(serializable.to_json()),
485 file_path: None,
486 pre_state: None,
487 post_state: None,
488 affected_symbols: vec![],
489 });
490
491 log
492 }
493
494 #[test]
495 fn test_replay_engine_analyze() {
496 let log = create_test_log_with_mutation();
497 let engine = ReplayEngine::new(&log);
498 let analysis = engine.analyze();
499
500 assert_eq!(analysis.total_mutations, 1);
501 assert_eq!(analysis.replayable, 1);
502 assert!(analysis.is_fully_replayable());
503 }
504
505 #[test]
506 #[ignore = "V1 path disabled - Mutation::apply removed, needs V2 migration"]
507 fn test_replay_simple_mutation() {
508 let log = create_test_log_with_mutation();
509 let mut engine = ReplayEngine::new(&log);
510
511 let mut file = PureFile::from_source("fn foo() {}").unwrap();
513
514 let result = engine.replay_all(&mut file).unwrap();
515
516 assert_eq!(result.mutations_applied, 1);
517 assert!(result.total_changes > 0);
518
519 let source = file.to_source().unwrap();
521 assert!(source.contains("bar"));
522 assert!(!source.contains("foo"));
523 }
524
525 #[test]
526 fn test_analyze_missing_data() {
527 let mut log = TxLog::new();
528
529 log.log(TxAction::MutationApplied {
531 mutation_type: "Rename".to_string(),
532 target: "foo -> bar".to_string(),
533 changes: 1,
534 mutation_data: None, file_path: None,
536 pre_state: None,
537 post_state: None,
538 affected_symbols: vec![],
539 });
540
541 let engine = ReplayEngine::new(&log);
542 let analysis = engine.analyze();
543
544 assert_eq!(analysis.total_mutations, 1);
545 assert_eq!(analysis.missing_data, 1);
546 assert_eq!(analysis.replayable, 0);
547 assert!(!analysis.is_fully_replayable());
548 }
549
550 #[test]
556 fn test_verify_multiple_mutation_types() {
557 use ryo_mutations::{AddDeriveMutation, AddFunctionMutation, ToSerializable};
558
559 let mut log = TxLog::new();
560
561 let rename = RenameMutation {
563 symbol_id: dummy_symbol_id(),
564 to: "new_name".to_string(),
565 };
566 log.log(TxAction::MutationApplied {
567 mutation_type: "Rename".to_string(),
568 target: "old_name -> new_name".to_string(),
569 changes: 1,
570 mutation_data: Some(rename.to_serializable().to_json()),
571 file_path: None,
572 pre_state: None,
573 post_state: None,
574 affected_symbols: vec![],
575 });
576
577 let add_fn = AddFunctionMutation {
579 parent: dummy_symbol_id(),
580 name: "new_fn".to_string(),
581 params: vec![("x".to_string(), "i32".to_string())],
582 return_type: Some("i32".to_string()),
583 body: "x + 1".to_string(),
584 is_pub: true,
585 };
586 log.log(TxAction::MutationApplied {
587 mutation_type: "AddFunction".to_string(),
588 target: "new_fn".to_string(),
589 changes: 1,
590 mutation_data: Some(add_fn.to_serializable().to_json()),
591 file_path: None,
592 pre_state: None,
593 post_state: None,
594 affected_symbols: vec![],
595 });
596
597 let add_derive = AddDeriveMutation {
599 symbol_id: dummy_symbol_id(),
600 derives: vec!["Debug".to_string(), "Clone".to_string()],
601 };
602 log.log(TxAction::MutationApplied {
603 mutation_type: "AddDerive".to_string(),
604 target: "MyStruct".to_string(),
605 changes: 1,
606 mutation_data: Some(add_derive.to_serializable().to_json()),
607 file_path: None,
608 pre_state: None,
609 post_state: None,
610 affected_symbols: vec![],
611 });
612
613 log.log(TxAction::MutationApplied {
615 mutation_type: "Rename".to_string(),
616 target: "legacy call".to_string(),
617 changes: 1,
618 mutation_data: None, file_path: None,
620 pre_state: None,
621 post_state: None,
622 affected_symbols: vec![],
623 });
624
625 let engine = ReplayEngine::new(&log);
626 let analysis = engine.analyze();
627
628 assert_eq!(analysis.total_mutations, 4);
634 assert_eq!(analysis.replayable, 1); assert_eq!(analysis.missing_data, 1); assert!(!analysis.is_fully_replayable()); assert!((analysis.replayable_percentage() - 25.0).abs() < 0.1);
640 }
641
642 #[test]
644 fn test_verify_generic_mutation_not_replayable() {
645 use ryo_mutations::SerializableMutation;
646
647 let mut log = TxLog::new();
648
649 let generic = SerializableMutation::Generic {
651 mutation_type: "CustomMutation".to_string(),
652 description: "Some custom operation".to_string(),
653 };
654
655 log.log(TxAction::MutationApplied {
656 mutation_type: "CustomMutation".to_string(),
657 target: "custom".to_string(),
658 changes: 1,
659 mutation_data: Some(generic.to_json()),
660 file_path: None,
661 pre_state: None,
662 post_state: None,
663 affected_symbols: vec![],
664 });
665
666 let engine = ReplayEngine::new(&log);
667 let analysis = engine.analyze();
668
669 assert_eq!(analysis.total_mutations, 1);
670 assert_eq!(analysis.replayable, 0); assert!(!analysis.unreplayable_types.is_empty());
672 }
673
674 #[test]
676 fn test_verify_replay_with_state_verification() {
677 use crate::storage::InMemoryStateStore;
678 use ryo_mutations::ToSerializable;
679
680 let mut state_store = InMemoryStateStore::new();
681
682 let initial_source = "fn foo() { println!(\"hello\"); }";
684 let pre_state = state_store.store(initial_source);
685
686 let expected_source = "fn bar() { println!(\"hello\"); }";
688 let post_state = state_store.store(expected_source);
689
690 let mut log = TxLog::new();
692 let rename = RenameMutation {
693 symbol_id: dummy_symbol_id(),
694 to: "bar".to_string(),
695 };
696
697 log.log(TxAction::MutationApplied {
698 mutation_type: "Rename".to_string(),
699 target: "foo -> bar".to_string(),
700 changes: 1,
701 mutation_data: Some(rename.to_serializable().to_json()),
702 file_path: Some(std::path::PathBuf::from("test.rs")),
703 pre_state: Some(pre_state.clone()),
704 post_state: Some(post_state.clone()),
705 affected_symbols: vec![],
706 });
707
708 let engine = ReplayEngine::new(&log);
709 let analysis = engine.analyze();
710
711 assert_eq!(analysis.with_pre_state, 1);
713 assert_eq!(analysis.with_post_state, 1);
714 assert!(analysis.can_verify());
715 }
716
717 #[test]
719 #[ignore = "V1 path disabled - Mutation::apply removed, needs V2 migration"]
720 fn test_verify_sequential_replay() {
721 use ryo_mutations::ToSerializable;
722
723 let mut log = TxLog::new();
724
725 let rename1 = RenameMutation {
727 symbol_id: dummy_symbol_id(),
728 to: "bar".to_string(),
729 };
730 log.log(TxAction::MutationApplied {
731 mutation_type: "Rename".to_string(),
732 target: "foo -> bar".to_string(),
733 changes: 1,
734 mutation_data: Some(rename1.to_serializable().to_json()),
735 file_path: None,
736 pre_state: None,
737 post_state: None,
738 affected_symbols: vec![],
739 });
740
741 let rename2 = RenameMutation {
743 symbol_id: dummy_symbol_id(),
744 to: "baz".to_string(),
745 };
746 log.log(TxAction::MutationApplied {
747 mutation_type: "Rename".to_string(),
748 target: "bar -> baz".to_string(),
749 changes: 1,
750 mutation_data: Some(rename2.to_serializable().to_json()),
751 file_path: None,
752 pre_state: None,
753 post_state: None,
754 affected_symbols: vec![],
755 });
756
757 let mut engine = ReplayEngine::new(&log);
759 let mut file = PureFile::from_source("fn foo() { foo(); }").unwrap();
760
761 let result = engine.replay_all(&mut file).unwrap();
762
763 assert_eq!(result.mutations_applied, 2);
765 let source = file.to_source().unwrap();
766 assert!(source.contains("baz"));
767 assert!(!source.contains("foo"));
768 assert!(!source.contains("bar"));
769 }
770
771 #[test]
773 fn test_verify_current_logger_issue() {
774 use crate::txlog::TxLogger;
775 use std::path::PathBuf;
776 use std::thread;
777 use std::time::Duration;
778
779 let logger = TxLogger::start(PathBuf::from("/test"), 10);
781
782 logger.log_mutation("Rename", "foo -> bar", 5);
784
785 thread::sleep(Duration::from_millis(10));
786 let log = logger.finish();
787
788 let engine = ReplayEngine::new(&log);
789 let analysis = engine.analyze();
790
791 assert_eq!(analysis.total_mutations, 1);
793 assert_eq!(analysis.missing_data, 1);
794 assert_eq!(analysis.replayable, 0);
795
796 }
799
800 #[test]
802 fn test_verify_new_record_mutation_api() {
803 use crate::txlog::TxLogger;
804 use std::path::PathBuf;
805 use std::thread;
806 use std::time::Duration;
807
808 let logger = TxLogger::start(PathBuf::from("/test"), 10);
809
810 let mutation = RenameMutation {
812 symbol_id: dummy_symbol_id(),
813 to: "bar".to_string(),
814 };
815 logger.record_mutation(&mutation, 5);
816
817 thread::sleep(Duration::from_millis(10));
818 let log = logger.finish();
819
820 let engine = ReplayEngine::new(&log);
821 let analysis = engine.analyze();
822
823 assert_eq!(analysis.total_mutations, 1);
825 assert_eq!(analysis.missing_data, 0);
826 assert_eq!(analysis.replayable, 1);
827 assert!(analysis.is_fully_replayable());
828 }
829
830 #[test]
832 fn test_verify_mixed_api_usage() {
833 use crate::txlog::TxLogger;
834 use std::path::PathBuf;
835 use std::thread;
836 use std::time::Duration;
837
838 let logger = TxLogger::start(PathBuf::from("/test"), 10);
839
840 logger.log_mutation("Rename", "legacy", 1);
842
843 let mutation1 = RenameMutation {
845 symbol_id: dummy_symbol_id(),
846 to: "bar".to_string(),
847 };
848 logger.record_mutation(&mutation1, 2);
849
850 let mutation2 = RenameMutation {
852 symbol_id: dummy_symbol_id(),
853 to: "qux".to_string(),
854 };
855 logger.record_mutation_for_file(&mutation2, 3, "src/lib.rs");
856
857 thread::sleep(Duration::from_millis(10));
858 let log = logger.finish();
859
860 let engine = ReplayEngine::new(&log);
861 let analysis = engine.analyze();
862
863 assert_eq!(analysis.total_mutations, 3);
865 assert_eq!(analysis.replayable, 2);
866 assert_eq!(analysis.missing_data, 1);
867 assert!(!analysis.is_fully_replayable());
868
869 assert!((analysis.replayable_percentage() - 66.67).abs() < 1.0);
871 }
872}