1use super::blueprint::ParallelBlueprint;
40use super::registry::MutationRegistry;
41use super::spec::{MutationSpec, MutationTargetSymbol};
42use crate::engine::{collect_affected_ids, MutationEvent};
43use ryo_analysis::{AnalysisContext, RegistryUpdateBatch, SymbolPath};
44use ryo_source::pure::ToSynError;
45use ryo_symbol::{MetadataError, WorkspaceFilePath};
46use std::collections::HashSet;
47use std::sync::Arc;
48use tracing::{debug, info, instrument, warn};
49
50#[derive(Debug, thiserror::Error)]
52pub enum SyncError {
53 #[error("cargo metadata unavailable: {0}")]
54 Metadata(#[from] MetadataError),
55 #[error("source generation failed: {0}")]
56 SourceGeneration(#[from] ToSynError),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum ExecutionStrategy {
62 #[default]
64 Sequential,
65
66 Wavefront,
69}
70
71pub fn suggest_strategy(blueprint: &ParallelBlueprint) -> ExecutionStrategy {
73 let parallelism = blueprint.parallelism();
74 let mutation_count = blueprint.mutations.len();
75
76 if parallelism <= 1.2 || mutation_count <= 2 {
81 ExecutionStrategy::Sequential
82 } else {
83 ExecutionStrategy::Wavefront
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct BlueprintResult {
90 pub results: Vec<SpecResult>,
92
93 pub total_changes: usize,
95
96 pub modified_files: Vec<WorkspaceFilePath>,
98
99 pub success: bool,
101
102 pub error: Option<String>,
104
105 pub registry_updates: RegistryUpdateBatch,
110}
111
112#[derive(Debug, Clone)]
114pub struct SpecResult {
115 pub index: usize,
117
118 pub spec_type: String,
120
121 pub changes: usize,
123
124 pub affected_files: Vec<WorkspaceFilePath>,
126
127 pub affected_symbols: Vec<SymbolPath>,
129
130 pub success: bool,
132
133 pub error: Option<String>,
135
136 pub registry_updates: RegistryUpdateBatch,
138
139 pub events: Vec<MutationEvent>,
141}
142
143impl BlueprintResult {
144 pub fn success(results: Vec<SpecResult>, modified_files: Vec<WorkspaceFilePath>) -> Self {
145 let total_changes = results.iter().map(|r| r.changes).sum();
146 let mut all_updates = RegistryUpdateBatch::new();
148 for result in &results {
149 for update in &result.registry_updates {
150 all_updates.push(update.clone());
151 }
152 }
153 Self {
154 results,
155 total_changes,
156 modified_files,
157 success: true,
158 error: None,
159 registry_updates: all_updates,
160 }
161 }
162
163 pub fn failure(error: impl Into<String>) -> Self {
164 Self {
165 results: vec![],
166 total_changes: 0,
167 modified_files: vec![],
168 success: false,
169 error: Some(error.into()),
170 registry_updates: RegistryUpdateBatch::new(),
171 }
172 }
173}
174
175#[derive(Debug)]
177pub struct BlueprintExecutor {
178 registry: MutationRegistry,
180
181 pub strategy: ExecutionStrategy,
183
184 pub verify_after_each: bool,
186
187 pub stop_on_error: bool,
189
190 pub ignore_conflicts: bool,
193}
194
195impl Default for BlueprintExecutor {
196 fn default() -> Self {
197 Self {
198 registry: MutationRegistry::default(),
199 strategy: ExecutionStrategy::default(),
200 verify_after_each: false,
201 stop_on_error: true,
202 ignore_conflicts: true,
203 }
204 }
205}
206
207pub(crate) fn ws_file_to_crate_relative_key<P: ryo_symbol::WorkspaceMetadataProvider>(
217 wfp: &WorkspaceFilePath,
218 metadata: &P,
219) -> Option<String> {
220 use ryo_symbol::CrateName;
221 let crate_name = CrateName::new(wfp.crate_name().as_str()).ok()?;
222 let layout = metadata.crate_layout(&crate_name)?;
223 let ws_rel = wfp.as_relative();
224 let prefix = layout.to_workspace_relative("");
225 let prefix_path = prefix.as_path();
226 let stripped = if prefix_path.as_os_str().is_empty() {
227 ws_rel.to_path_buf()
228 } else {
229 ws_rel.strip_prefix(prefix_path).ok()?.to_path_buf()
230 };
231 stripped.to_str().map(str::to_string)
232}
233
234pub(crate) fn ws_file_to_crate_pair<P: ryo_symbol::WorkspaceMetadataProvider>(
243 wfp: &WorkspaceFilePath,
244 metadata: &P,
245) -> Option<(ryo_symbol::CrateName, String)> {
246 use ryo_symbol::CrateName;
247 let crate_name = CrateName::new(wfp.crate_name().as_str()).ok()?;
248 let key = ws_file_to_crate_relative_key(wfp, metadata)?;
249 Some((crate_name, key))
250}
251
252impl BlueprintExecutor {
253 pub fn new() -> Self {
254 Self::default()
255 }
256
257 #[instrument(skip(self, blueprint, ctx), fields(mutations = blueprint.mutations.len()))]
274 pub fn execute_v2(
275 &self,
276 blueprint: &ParallelBlueprint,
277 ctx: &mut AnalysisContext,
278 ) -> BlueprintResult {
279 debug!(
280 "Starting blueprint execution with {} mutations",
281 blueprint.mutations.len()
282 );
283
284 if !self.ignore_conflicts && blueprint.needs_escalation() {
286 warn!(
287 "Blueprint has {} conflicts requiring escalation",
288 blueprint.conflicts.len()
289 );
290 return BlueprintResult::failure(format!(
291 "Blueprint has {} conflicts requiring escalation",
292 blueprint.conflicts.len()
293 ));
294 }
295
296 let mut results: Vec<SpecResult> = Vec::new();
297 let mut completed: HashSet<usize> = HashSet::new();
298
299 while completed.len() < blueprint.mutations.len() {
301 let ready = blueprint
302 .deps
303 .ready_set(blueprint.mutations.len(), &completed);
304
305 if ready.is_empty() {
306 if completed.len() < blueprint.mutations.len() {
307 warn!("Dependency cycle detected");
308 return BlueprintResult::failure("Dependency cycle detected");
309 }
310 break;
311 }
312
313 debug!("Ready to execute {} specs", ready.len());
314
315 for idx in ready {
316 let spec = &blueprint.mutations[idx];
317 debug!("Executing spec {}: {:?}", idx, spec);
318 let result = self.execute_spec_v2(idx, spec, ctx);
319
320 if let Some(ref error) = result.error {
321 warn!(
322 "Spec {} completed with error: success={}, changes={}, error={}",
323 idx, result.success, result.changes, error
324 );
325 } else {
326 debug!(
327 "Spec {} completed: success={}, changes={}, events={}",
328 idx,
329 result.success,
330 result.changes,
331 result.events.len()
332 );
333 }
334
335 let success = result.success;
336 results.push(result);
337 completed.insert(idx);
338
339 if !success && self.stop_on_error {
340 let total_changes = results.iter().map(|r| r.changes).sum();
341
342 let spec_error = results
344 .last()
345 .and_then(|r| r.error.as_ref())
346 .map(|e| format!("Spec {} failed: {}", idx, e))
347 .unwrap_or_else(|| format!("Stopped at spec {} (no error message)", idx));
348
349 warn!("Stopping on error at spec {}: {}", idx, spec_error);
350
351 return BlueprintResult {
352 results,
353 total_changes,
354 modified_files: Vec::new(),
355 success: false,
356 error: Some(spec_error),
357 registry_updates: RegistryUpdateBatch::new(),
358 };
359 }
360 }
361 }
362
363 let total_changes: usize = results.iter().map(|r| r.changes).sum();
364 info!(
365 "Blueprint execution completed: {} results, {} total changes",
366 results.len(),
367 total_changes
368 );
369
370 BlueprintResult::success(results, Vec::new())
371 }
372
373 #[instrument(skip(result, ctx), fields(total_changes = result.total_changes))]
407 pub fn sync_files_and_rebuild(
408 result: &BlueprintResult,
409 ctx: &mut AnalysisContext,
410 ) -> Result<Vec<WorkspaceFilePath>, SyncError> {
411 use crate::engine::{collect_modified_symbols, RegistryGenerator};
412 use ryo_symbol::CargoMetadataProvider;
413
414 debug!(
415 "Starting sync_files_and_rebuild with {} results",
416 result.results.len()
417 );
418
419 let all_events: Vec<MutationEvent> = result
421 .results
422 .iter()
423 .flat_map(|r| r.events.clone())
424 .collect();
425
426 debug!(
427 "Collected {} mutation events from results",
428 all_events.len()
429 );
430 if all_events.is_empty() {
433 debug!("No mutation events — skipping file sync entirely");
434 return Ok(Vec::new());
435 }
436
437 let modified_symbol_ids = collect_modified_symbols(&all_events, ctx.registry());
439 debug!("Found {} modified symbols", modified_symbol_ids.len());
440 let metadata = CargoMetadataProvider::from_directory(&ctx.workspace_root)?;
449
450 let mut existing_per_crate: std::collections::HashMap<
462 ryo_symbol::CrateName,
463 std::collections::HashSet<String>,
464 > = std::collections::HashMap::new();
465 for wfp in ctx.files().keys() {
466 if let Some((crate_name, key)) = ws_file_to_crate_pair(wfp, &metadata) {
467 existing_per_crate
468 .entry(crate_name)
469 .or_default()
470 .insert(key);
471 }
472 }
473
474 let lib_crates: std::collections::HashSet<ryo_symbol::CrateName> = existing_per_crate
482 .iter()
483 .filter_map(|(crate_name, keys)| {
484 if keys.contains("src/lib.rs") {
485 Some(crate_name.clone())
486 } else {
487 None
488 }
489 })
490 .collect();
491
492 let generator = RegistryGenerator::multi_file()
493 .with_existing_files_per_crate(existing_per_crate)
494 .with_lib_crates(lib_crates);
495 let workspace = generator.generate_affected(
496 &ctx.ast_registry,
497 ctx.registry(),
498 &modified_symbol_ids,
499 &metadata,
500 )?;
501
502 debug!(
503 "Generator produced {} crates with {} total files",
504 workspace.crates.len(),
505 workspace.total_files()
506 );
507
508 let mut modified_files = Vec::new();
512
513 for generated_crate in workspace.crates.values() {
514 debug!(
515 "Processing crate {} with {} files",
516 generated_crate.crate_name,
517 generated_crate.files.len()
518 );
519
520 use ryo_symbol::{CrateName, WorkspacePathResolver};
522 let crate_name = CrateName::new(&generated_crate.crate_name).expect(
523 "generator-emitted crate names must already satisfy CrateName validation; \
524 reaching this expect means the generator is producing invalid names",
525 );
526 let layout = metadata.crate_layout(&crate_name);
527
528 for (crate_relative_path, generated_file) in &generated_crate.files {
529 let workspace_relative = match &layout {
537 Some(layout) => layout.to_workspace_relative(crate_relative_path),
538 None => {
539 std::path::PathBuf::from(crate_relative_path.as_str())
541 }
542 };
543
544 let workspace_file = ctx
547 .files()
548 .keys()
549 .find(|wfp| {
550 wfp.crate_name().as_str() == generated_crate.crate_name
551 && wfp.as_relative() == workspace_relative.as_path()
552 })
553 .cloned();
554
555 let wfp = if let Some(existing) = workspace_file {
556 debug!(
557 "Updating existing file: {} ({})",
558 crate_relative_path,
559 existing.as_relative().display()
560 );
561 existing
562 } else {
563 debug!(
565 "Creating new file: {} -> {} for crate {}",
566 crate_relative_path,
567 workspace_relative.display(),
568 generated_crate.crate_name
569 );
570 let resolver =
571 WorkspacePathResolver::new(ctx.workspace_root.as_ref().to_path_buf());
572 resolver.resolve_relative_with_crate(&workspace_relative, crate_name.clone())
573 };
574
575 let parsed = ryo_source::pure::PureFile::from_source(&generated_file.source);
576 let pure_file = parsed.unwrap_or_else(|_e| ryo_source::pure::PureFile::new());
577 ctx.files_mut().insert(wfp.clone(), Arc::new(pure_file));
578 modified_files.push(wfp);
579 }
580 }
581
582 debug!(
583 "Updated {} files in context from generator output",
584 modified_files.len()
585 );
586
587 if !all_events.is_empty() {
589 let affected_ids = collect_affected_ids(&all_events, ctx.registry());
590 debug!("Rebuilding with {} affected symbol IDs", affected_ids.len());
591 ctx.rebuild_after_mutation_by_symbols(&affected_ids);
592 } else if !modified_files.is_empty() {
593 debug!(
595 "Rebuilding with {} modified files (fallback)",
596 modified_files.len()
597 );
598 ctx.rebuild_after_mutation(&modified_files);
599 }
600
601 info!(
602 "sync_files_and_rebuild completed: {} files modified",
603 modified_files.len()
604 );
605 Ok(modified_files)
606 }
607
608 fn execute_spec_v2(
613 &self,
614 index: usize,
615 spec: &MutationSpec,
616 ctx: &mut AnalysisContext,
617 ) -> SpecResult {
618 use crate::engine::ASTMutationEngine;
619
620 let spec_type = spec_type_name(spec);
621 let affected_symbols: Vec<SymbolPath> = spec
623 .get_targets()
624 .iter()
625 .filter_map(|target| target.to_path(ctx.registry()))
626 .collect();
627
628 match self.registry.convert_v2(spec, ctx) {
630 Ok(mutations) => {
631 let exec_result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, ctx);
632
633 if let MutationSpec::AddItem {
636 target, content, ..
637 } = spec
638 {
639 if let MutationTargetSymbol::ByPath(path) = target {
641 register_item_from_content(ctx, path, content);
642 } else if let MutationTargetSymbol::ById(id) = target {
643 if let Some(path) = ctx.registry().resolve(*id).cloned() {
645 register_item_from_content(ctx, &path, content);
646 }
647 }
648 }
649
650 SpecResult {
651 index,
652 spec_type,
653 success: true,
654 changes: exec_result.result.changes,
655 affected_files: vec![], affected_symbols,
657 error: None,
658 registry_updates: RegistryUpdateBatch::new(),
659 events: exec_result.events,
660 }
661 }
662 Err(e) => SpecResult {
663 index,
664 spec_type,
665 success: false,
666 changes: 0,
667 affected_files: vec![],
668 affected_symbols,
669 error: Some(format!("convert_v2 failed: {}", e)),
670 registry_updates: RegistryUpdateBatch::new(),
671 events: vec![],
672 },
673 }
674 }
675
676 pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
678 self.strategy = strategy;
679 self
680 }
681
682 pub fn with_verify(mut self, verify: bool) -> Self {
684 self.verify_after_each = verify;
685 self
686 }
687
688 pub fn with_stop_on_error(mut self, stop: bool) -> Self {
690 self.stop_on_error = stop;
691 self
692 }
693}
694
695fn register_item_from_content(
700 ctx: &mut AnalysisContext,
701 target: &ryo_symbol::SymbolPath,
702 content: &str,
703) {
704 use ryo_symbol::SymbolKind;
705
706 let trimmed = content.trim();
707
708 let mut lines = trimmed.lines();
710 let mut decl_line = "";
711 for line in lines.by_ref() {
712 let line = line.trim();
713 if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
714 decl_line = line;
715 break;
716 }
717 }
718
719 let tokens: Vec<&str> = decl_line.split_whitespace().collect();
721 if tokens.is_empty() {
722 return;
723 }
724
725 let mut idx = 0;
726
727 if tokens.get(idx) == Some(&"pub") {
729 idx += 1;
730 if let Some(t) = tokens.get(idx) {
732 if t.starts_with('(') {
733 idx += 1;
734 }
735 }
736 }
737
738 let Some(keyword) = tokens.get(idx) else {
740 return;
741 };
742 idx += 1;
743
744 let kind = match *keyword {
745 "struct" => SymbolKind::Struct,
746 "enum" => SymbolKind::Enum,
747 "fn" => SymbolKind::Function,
748 "type" => SymbolKind::TypeAlias,
749 "const" => SymbolKind::Const,
750 "static" => SymbolKind::Static,
751 "trait" => SymbolKind::Trait,
752 "mod" => SymbolKind::Mod,
753 "impl" => return, _ => return,
755 };
756
757 let Some(name_token) = tokens.get(idx) else {
759 return;
760 };
761 let name = name_token
762 .split(['<', '{', '(', ':'])
763 .next()
764 .unwrap_or(name_token);
765
766 let target_str = target.to_string();
768 let is_crate_root = target_str == "crate";
769 let full_path = if is_crate_root {
770 ryo_symbol::SymbolPath::parse(&format!("crate::{}", name))
771 } else {
772 ryo_symbol::SymbolPath::parse(&format!("{}::{}", target_str, name))
773 };
774
775 let Ok(path) = full_path else {
776 return;
777 };
778
779 let _ = ctx.registry_mut().register(path, kind);
781}
782
783fn spec_type_name(spec: &MutationSpec) -> String {
785 match spec {
786 MutationSpec::Rename { .. } => "Rename".to_string(),
787 MutationSpec::AddField { .. } => "AddField".to_string(),
788 MutationSpec::RemoveField { .. } => "RemoveField".to_string(),
789 MutationSpec::ChangeVisibility { .. } => "ChangeVisibility".to_string(),
790 MutationSpec::AddDerive { .. } => "AddDerive".to_string(),
791 MutationSpec::RemoveDerive { .. } => "RemoveDerive".to_string(),
792 MutationSpec::AddVariant { .. } => "AddVariant".to_string(),
793 MutationSpec::RemoveVariant { .. } => "RemoveVariant".to_string(),
794 MutationSpec::AddMatchArm { .. } => "AddMatchArm".to_string(),
795 MutationSpec::RemoveMatchArm { .. } => "RemoveMatchArm".to_string(),
796 MutationSpec::ReplaceMatchArm { .. } => "ReplaceMatchArm".to_string(),
797 MutationSpec::AddStructLiteralField { .. } => "AddStructLiteralField".to_string(),
798 MutationSpec::RemoveStructLiteralField { .. } => "RemoveStructLiteralField".to_string(),
799 MutationSpec::AddItem { .. } => "AddItem".to_string(),
800 MutationSpec::RemoveItem { .. } => "RemoveItem".to_string(),
801 MutationSpec::ReplaceCode { .. } => "ReplaceCode".to_string(),
802 MutationSpec::AddMethod { .. } => "AddMethod".to_string(),
803 MutationSpec::RemoveMethod { .. } => "RemoveMethod".to_string(),
804 MutationSpec::RemoveMod { .. } => "RemoveMod".to_string(),
805 MutationSpec::CreateMod { .. } => "CreateMod".to_string(),
806 MutationSpec::OrganizeImports { .. } => "OrganizeImports".to_string(),
807 MutationSpec::LoopToIterator { .. } => "LoopToIterator".to_string(),
808 MutationSpec::UnwrapToQuestion { .. } => "UnwrapToQuestion".to_string(),
809 MutationSpec::AddSpec { .. } => "AddSpec".to_string(),
810 MutationSpec::RemoveSpec { .. } => "RemoveSpec".to_string(),
811 MutationSpec::ValidateSpec { .. } => "ValidateSpec".to_string(),
812 MutationSpec::ExtractTrait { .. } => "ExtractTrait".to_string(),
813 MutationSpec::InlineTrait { .. } => "InlineTrait".to_string(),
814 MutationSpec::ReplaceType { .. } => "ReplaceType".to_string(),
815 MutationSpec::EnumToTrait { .. } => "EnumToTrait".to_string(),
816 MutationSpec::MoveItem { .. } => "MoveItem".to_string(),
817 MutationSpec::AssignOp { .. } => "AssignOp".to_string(),
818 MutationSpec::BoolSimplify { .. } => "BoolSimplify".to_string(),
819 MutationSpec::CloneOnCopy { .. } => "CloneOnCopy".to_string(),
820 MutationSpec::CollapsibleIf { .. } => "CollapsibleIf".to_string(),
821 MutationSpec::NoOpArmToTodo { .. } => "NoOpArmToTodo".to_string(),
822 MutationSpec::ComparisonToMethod { .. } => "ComparisonToMethod".to_string(),
823 MutationSpec::RedundantClosure { .. } => "RedundantClosure".to_string(),
824 MutationSpec::IntroduceVariable { .. } => "IntroduceVariable".to_string(),
825 MutationSpec::ManualMap { .. } => "ManualMap".to_string(),
826 MutationSpec::MatchToIfLet { .. } => "MatchToIfLet".to_string(),
827 MutationSpec::FilterNext { .. } => "FilterNext".to_string(),
828 MutationSpec::MapUnwrapOr { .. } => "MapUnwrapOr".to_string(),
829 MutationSpec::ReplaceExpr { .. } => "ReplaceExpr".to_string(),
830 MutationSpec::RemoveStatement { .. } => "RemoveStatement".to_string(),
831 MutationSpec::InsertStatement { .. } => "InsertStatement".to_string(),
832 MutationSpec::ReplaceStatement { .. } => "ReplaceStatement".to_string(),
833 MutationSpec::PluginTransform { .. } => "PluginTransform".to_string(),
834 MutationSpec::DuplicateFunction { .. } => "DuplicateFunction".to_string(),
835 MutationSpec::DuplicateStruct { .. } => "DuplicateStruct".to_string(),
836 MutationSpec::DuplicateEnum { .. } => "DuplicateEnum".to_string(),
837 MutationSpec::DuplicateModTree { .. } => "DuplicateModTree".to_string(),
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844 use crate::executor::spec::{Scope, SelfParam, SymbolPath, Visibility};
845 use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
846 use ryo_symbol::SymbolId;
847
848 fn dummy_id(index: u32) -> SymbolId {
850 SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
851 }
852
853 fn execute_and_sync(
856 executor: &BlueprintExecutor,
857 blueprint: &ParallelBlueprint,
858 ctx: &mut AnalysisContext,
859 ) -> BlueprintResult {
860 let result = executor.execute_v2(blueprint, ctx);
861 if result.success {
862 BlueprintExecutor::sync_files_and_rebuild(&result, ctx).unwrap();
863 }
864 result
865 }
866
867 fn create_test_context() -> AnalysisContext {
868 let code = r#"
869struct Config {
870 name: String,
871}
872
873impl Config {
874 fn new() -> Self {
875 Self { name: String::new() }
876 }
877}
878"#;
879 ContextBuilder::new()
880 .with_file("src/config.rs", code)
881 .build()
882 }
883
884 #[test]
885 fn test_blueprint_executor_rename() {
886 let mut ctx = create_test_context();
887
888 let symbol_id = ctx
890 .registry()
891 .lookup_by_name("Config")
892 .expect("Config should exist in registry");
893
894 let specs = vec![MutationSpec::Rename {
895 target: MutationTargetSymbol::ById(symbol_id),
896 to: "AppConfig".to_string(),
897 scope: Scope::Project,
898 }];
899 let blueprint = ParallelBlueprint::from_mutations(specs);
900
901 let executor = BlueprintExecutor::new();
902 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
903
904 assert!(exec_result.success);
905 assert!(exec_result.total_changes > 0);
906
907 let file = ctx.test_file("src/config.rs").unwrap();
909 let source = file.to_source().unwrap();
910 assert!(source.contains("AppConfig"));
911 assert!(!source.contains("struct Config"));
912 }
913
914 #[test]
915 fn test_blueprint_executor_add_derive() {
916 let mut ctx = create_test_context();
917
918 let path = SymbolPath::parse("test_crate::config::Config").unwrap();
920 let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
921
922 let specs = vec![MutationSpec::AddDerive {
923 target: MutationTargetSymbol::ById(symbol_id),
924 derives: vec!["Debug".to_string(), "Clone".to_string()],
925 }];
926 let blueprint = ParallelBlueprint::from_mutations(specs);
927
928 let executor = BlueprintExecutor::new();
929 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
930
931 assert!(exec_result.success);
932
933 let file = ctx.test_file("src/config.rs").unwrap();
934 let source = file.to_source().unwrap();
935 assert!(source.contains("derive"), "Expected derive in: {}", source);
936 assert!(source.contains("Debug"));
937 }
938
939 #[test]
940 fn test_blueprint_executor_organize_imports() {
941 let code = r#"
942use std::collections::HashMap;
943use std::io::Write;
944use std::collections::HashSet;
945use std::io::Read;
946
947fn main() {}
948"#;
949 let mut ctx = ContextBuilder::new().with_file("src/main.rs", code).build();
950
951 let specs = vec![MutationSpec::OrganizeImports {
952 module_id: None,
953 deduplicate: true,
954 merge_groups: true,
955 }];
956 let blueprint = ParallelBlueprint::from_mutations(specs);
957
958 let executor = BlueprintExecutor::new();
959 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
960
961 assert!(exec_result.success);
962 }
963
964 #[test]
965 fn test_blueprint_with_conflicts_fails_when_not_ignored() {
966 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
967
968 let mut ctx = create_test_context();
969
970 let mut symbol_registry = SymbolRegistry::new();
972 let path_a = SymbolPath::parse("test_crate::A").unwrap();
973 let symbol_a = symbol_registry
974 .register(path_a, SymbolKind::Struct)
975 .unwrap();
976
977 let specs = vec![
979 MutationSpec::Rename {
980 target: MutationTargetSymbol::ById(symbol_a),
981 to: "B".to_string(),
982 scope: Scope::Project,
983 },
984 MutationSpec::Rename {
985 target: MutationTargetSymbol::ById(symbol_a),
986 to: "C".to_string(),
987 scope: Scope::Project,
988 },
989 ];
990
991 let blueprint = ParallelBlueprint::from_mutations(specs);
992
993 assert!(
995 !blueprint.conflicts.is_empty(),
996 "Blueprint should have conflicts"
997 );
998
999 let mut executor = BlueprintExecutor::new();
1001 executor.ignore_conflicts = false;
1002 let result = execute_and_sync(&executor, &blueprint, &mut ctx);
1003
1004 assert!(
1006 !result.success,
1007 "Execution should fail when conflicts are not ignored"
1008 );
1009 assert!(result.error.is_some(), "Should have error message");
1010 assert!(
1011 result.error.unwrap().contains("conflict"),
1012 "Error should mention conflicts"
1013 );
1014 }
1015
1016 #[test]
1023 fn test_blueprint_executor_add_item_struct() {
1024 let mut ctx = ContextBuilder::new()
1025 .with_file("src/lib.rs", "// empty file\n")
1026 .build();
1027
1028 let spec = MutationSpec::AddItem {
1029 target: MutationTargetSymbol::ByPath(Box::new(
1030 SymbolPath::parse("test_crate").unwrap(),
1031 )),
1032 content: "pub struct Config {}".to_string(),
1033 position: super::super::spec::InsertPosition::Bottom,
1034 };
1035
1036 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1037 let executor = BlueprintExecutor::new();
1038 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1039
1040 assert!(
1041 exec_result.success,
1042 "AddItem struct failed: {:?}",
1043 exec_result.error
1044 );
1045
1046 let file = ctx.test_file("src/lib.rs").unwrap();
1047 let source = file.to_source().unwrap();
1048 assert!(
1049 source.contains("pub struct Config"),
1050 "Struct not added: {}",
1051 source
1052 );
1053 }
1054
1055 #[test]
1056 fn test_blueprint_executor_add_item_fn() {
1057 let mut ctx = ContextBuilder::new()
1058 .with_file("src/lib.rs", "// empty file\n")
1059 .build();
1060
1061 let spec = MutationSpec::AddItem {
1062 target: MutationTargetSymbol::ByPath(Box::new(
1063 SymbolPath::parse("test_crate").unwrap(),
1064 )),
1065 content: "fn helper() {}".to_string(),
1066 position: super::super::spec::InsertPosition::Bottom,
1067 };
1068
1069 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1070 let executor = BlueprintExecutor::new();
1071 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1072
1073 assert!(
1074 exec_result.success,
1075 "AddItem fn failed: {:?}",
1076 exec_result.error
1077 );
1078
1079 let file = ctx.test_file("src/lib.rs").unwrap();
1080 let source = file.to_source().unwrap();
1081 assert!(
1082 source.contains("fn helper"),
1083 "Function not added: {}",
1084 source
1085 );
1086 }
1087
1088 #[test]
1089 fn test_blueprint_executor_add_item_use() {
1090 let mut ctx = ContextBuilder::new()
1091 .with_file("src/lib.rs", "pub struct Dummy;\n")
1092 .build();
1093
1094 let spec = MutationSpec::AddItem {
1095 target: MutationTargetSymbol::ByPath(Box::new(
1096 SymbolPath::parse("test_crate").unwrap(),
1097 )),
1098 content: "use HashMap;".to_string(),
1099 position: super::super::spec::InsertPosition::Top,
1100 };
1101
1102 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1103 let executor = BlueprintExecutor::new();
1104 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1105
1106 assert!(
1107 exec_result.success,
1108 "AddItem use failed: {:?}",
1109 exec_result.error
1110 );
1111
1112 let file = ctx.test_file("src/lib.rs").unwrap();
1113 let source = file.to_source().unwrap();
1114 assert!(source.contains("use HashMap"), "Use not added: {}", source);
1115 }
1116
1117 #[test]
1118 fn test_blueprint_executor_add_item_impl() {
1119 let mut ctx = ContextBuilder::new()
1120 .with_file("src/lib.rs", "struct Foo {}\n")
1121 .build();
1122
1123 let spec = MutationSpec::AddItem {
1124 target: MutationTargetSymbol::ByPath(Box::new(
1125 SymbolPath::parse("test_crate").unwrap(),
1126 )),
1127 content: "impl Foo {}".to_string(),
1128 position: super::super::spec::InsertPosition::Bottom,
1129 };
1130
1131 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1132 let executor = BlueprintExecutor::new();
1133 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1134
1135 assert!(
1136 exec_result.success,
1137 "AddItem impl failed: {:?}",
1138 exec_result.error
1139 );
1140
1141 let file = ctx.test_file("src/lib.rs").unwrap();
1142 let source = file.to_source().unwrap();
1143 assert!(source.contains("impl Foo"), "Impl not added: {}", source);
1144 }
1145
1146 #[test]
1147 fn test_blueprint_executor_add_item_enum() {
1148 let mut ctx = ContextBuilder::new()
1149 .with_file("src/lib.rs", "// empty file\n")
1150 .build();
1151
1152 let spec = MutationSpec::AddItem {
1153 target: MutationTargetSymbol::ByPath(Box::new(
1154 SymbolPath::parse("test_crate").unwrap(),
1155 )),
1156 content: "pub enum Status { Pending, Active, Completed }".to_string(),
1157 position: super::super::spec::InsertPosition::Bottom,
1158 };
1159
1160 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1161 let executor = BlueprintExecutor::new();
1162 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1163
1164 assert!(
1165 exec_result.success,
1166 "AddItem enum failed: {:?}",
1167 exec_result.error
1168 );
1169
1170 let file = ctx.test_file("src/lib.rs").unwrap();
1171 let source = file.to_source().unwrap();
1172 assert!(
1173 source.contains("pub enum Status"),
1174 "Enum not added: {}",
1175 source
1176 );
1177 }
1178
1179 #[test]
1182 fn test_blueprint_executor_add_method_basic() {
1183 let mut ctx = ContextBuilder::new()
1184 .with_file("src/lib.rs", "pub struct Config {}\n\nimpl Config {}\n")
1185 .build();
1186
1187 let spec = MutationSpec::AddMethod {
1188 target: MutationTargetSymbol::ByKindAndName(
1189 crate::executor::ItemKind::Impl,
1190 "Config".to_string(),
1191 ),
1192 method_name: "new".to_string(),
1193 params: vec![],
1194 return_type: Some("Self".to_string()),
1195 body: "Self {}".to_string(),
1196 is_pub: true,
1197 self_param: None,
1198 };
1199
1200 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1201 let executor = BlueprintExecutor::new();
1202 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1203
1204 assert!(
1205 exec_result.success,
1206 "AddMethod basic failed: {:?}",
1207 exec_result.error
1208 );
1209
1210 let file = ctx.test_file("src/lib.rs").unwrap();
1211 let source = file.to_source().unwrap();
1212 assert!(
1213 source.contains("pub fn new"),
1214 "Method not added: {}",
1215 source
1216 );
1217 assert!(
1218 source.contains("-> Self"),
1219 "Return type not added: {}",
1220 source
1221 );
1222 }
1223
1224 #[test]
1225 fn test_blueprint_executor_add_method_with_self() {
1226 let mut ctx = ContextBuilder::new()
1227 .with_file(
1228 "src/lib.rs",
1229 "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
1230 )
1231 .build();
1232
1233 let spec = MutationSpec::AddMethod {
1234 target: MutationTargetSymbol::ByKindAndName(
1235 crate::executor::ItemKind::Impl,
1236 "Counter".to_string(),
1237 ),
1238 method_name: "get".to_string(),
1239 params: vec![],
1240 return_type: Some("u32".to_string()),
1241 body: "self.value".to_string(),
1242 is_pub: true,
1243 self_param: Some(SelfParam::Ref),
1244 };
1245
1246 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1247 let executor = BlueprintExecutor::new();
1248 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1249
1250 assert!(
1251 exec_result.success,
1252 "AddMethod with_self failed: {:?}",
1253 exec_result.error
1254 );
1255
1256 let file = ctx.test_file("src/lib.rs").unwrap();
1257 let source = file.to_source().unwrap();
1258 assert!(source.contains("&self"), "Self param not added: {}", source);
1259 assert!(source.contains("fn get"), "Method not added: {}", source);
1260 }
1261
1262 #[test]
1263 fn test_blueprint_executor_add_method_with_mut_self() {
1264 let mut ctx = ContextBuilder::new()
1265 .with_file(
1266 "src/lib.rs",
1267 "pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
1268 )
1269 .build();
1270
1271 let spec = MutationSpec::AddMethod {
1272 target: MutationTargetSymbol::ByKindAndName(
1273 crate::executor::ItemKind::Impl,
1274 "Counter".to_string(),
1275 ),
1276 method_name: "increment".to_string(),
1277 params: vec![],
1278 return_type: None,
1279 body: "self.value += 1".to_string(),
1280 is_pub: true,
1281 self_param: Some(SelfParam::Mut),
1282 };
1283
1284 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1285 let executor = BlueprintExecutor::new();
1286 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1287
1288 assert!(
1289 exec_result.success,
1290 "AddMethod with_mut_self failed: {:?}",
1291 exec_result.error
1292 );
1293
1294 let file = ctx.test_file("src/lib.rs").unwrap();
1295 let source = file.to_source().unwrap();
1296 assert!(
1297 source.contains("&mut self"),
1298 "Mut self param not added: {}",
1299 source
1300 );
1301 assert!(
1302 source.contains("fn increment"),
1303 "Method not added: {}",
1304 source
1305 );
1306 }
1307
1308 #[test]
1309 fn test_blueprint_executor_add_method_with_params() {
1310 let mut ctx = ContextBuilder::new()
1311 .with_file(
1312 "src/lib.rs",
1313 "pub struct Calculator {}\n\nimpl Calculator {}\n",
1314 )
1315 .build();
1316
1317 let spec = MutationSpec::AddMethod {
1318 target: MutationTargetSymbol::ByKindAndName(
1319 crate::executor::ItemKind::Impl,
1320 "Calculator".to_string(),
1321 ),
1322 method_name: "add".to_string(),
1323 params: vec![
1324 ("a".to_string(), "i32".to_string()),
1325 ("b".to_string(), "i32".to_string()),
1326 ],
1327 return_type: Some("i32".to_string()),
1328 body: "a + b".to_string(),
1329 is_pub: true,
1330 self_param: None,
1331 };
1332
1333 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1334 let executor = BlueprintExecutor::new();
1335 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1336
1337 assert!(
1338 exec_result.success,
1339 "AddMethod with_params failed: {:?}",
1340 exec_result.error
1341 );
1342
1343 let file = ctx.test_file("src/lib.rs").unwrap();
1344 let source = file.to_source().unwrap();
1345 assert!(source.contains("fn add"), "Method not added: {}", source);
1346 assert!(source.contains("a: i32"), "Param a not added: {}", source);
1347 assert!(source.contains("b: i32"), "Param b not added: {}", source);
1348 }
1349
1350 #[test]
1353 fn test_blueprint_executor_multi_file_rename() {
1354 let mut ctx = ContextBuilder::new()
1355 .with_file(
1356 "src/lib.rs",
1357 r#"mod models;
1358
1359fn process(task: Task) -> Task {
1360 task
1361}
1362"#,
1363 )
1364 .with_file(
1365 "src/models.rs",
1366 r#"pub struct Task {
1367 pub id: u32,
1368 pub name: String,
1369}
1370"#,
1371 )
1372 .build();
1373
1374 let symbol_id = ctx
1376 .registry()
1377 .lookup_by_name("Task")
1378 .expect("Task should exist in registry");
1379
1380 let spec = MutationSpec::Rename {
1382 target: MutationTargetSymbol::ById(symbol_id),
1383 to: "TodoItem".to_string(),
1384 scope: Scope::Project,
1385 };
1386
1387 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1388 let executor = BlueprintExecutor::new();
1389 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1390
1391 assert!(
1392 exec_result.success,
1393 "Multi-file rename failed: {:?}",
1394 exec_result.error
1395 );
1396
1397 let lib = ctx.test_file("src/lib.rs").unwrap();
1399 let lib_source = lib.to_source().unwrap();
1400 assert!(
1401 lib_source.contains("task: TodoItem"),
1402 "lib.rs type not renamed: {}",
1403 lib_source
1404 );
1405 assert!(
1406 lib_source.contains("-> TodoItem"),
1407 "lib.rs return type not renamed: {}",
1408 lib_source
1409 );
1410
1411 let models = ctx.test_file("src/models.rs").unwrap();
1413 let models_source = models.to_source().unwrap();
1414 assert!(
1415 models_source.contains("struct TodoItem"),
1416 "models.rs struct not renamed: {}",
1417 models_source
1418 );
1419 assert!(
1420 !models_source.contains("struct Task"),
1421 "models.rs still has struct Task: {}",
1422 models_source
1423 );
1424 }
1425
1426 #[test]
1427 fn test_blueprint_executor_multi_file_add_items() {
1428 let mut ctx = ContextBuilder::new()
1429 .with_file("src/lib.rs", "pub mod models;\n")
1430 .with_file("src/models.rs", "pub struct Placeholder;\n")
1431 .build();
1432
1433 let spec1 = MutationSpec::AddItem {
1435 target: MutationTargetSymbol::ByPath(Box::new(
1436 SymbolPath::parse("test_crate::models").unwrap(),
1437 )),
1438 content: "pub struct User { id: u32 }".to_string(),
1439 position: super::super::spec::InsertPosition::Bottom,
1440 };
1441
1442 let spec2 = MutationSpec::AddItem {
1444 target: MutationTargetSymbol::ByPath(Box::new(
1445 SymbolPath::parse("test_crate").unwrap(),
1446 )),
1447 content: "use models::User;".to_string(),
1448 position: super::super::spec::InsertPosition::Bottom,
1449 };
1450
1451 let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
1452 let executor = BlueprintExecutor::new();
1453 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1454
1455 assert!(
1456 exec_result.success,
1457 "Multi-file add items failed: {:?}",
1458 exec_result.error
1459 );
1460
1461 let models = ctx.test_file("src/models.rs").unwrap();
1463 let models_source = models.to_source().unwrap();
1464 assert!(
1465 models_source.contains("pub struct User"),
1466 "User not added to models.rs: {}",
1467 models_source
1468 );
1469
1470 let lib = ctx.test_file("src/lib.rs").unwrap();
1472 let lib_source = lib.to_source().unwrap();
1473 assert!(
1474 lib_source.contains("use models::User"),
1475 "Use not added to lib.rs: {}",
1476 lib_source
1477 );
1478 }
1479
1480 #[test]
1483 fn test_blueprint_executor_add_item_generic_struct() {
1484 let mut ctx = ContextBuilder::new()
1485 .with_file("src/lib.rs", "// empty file\n")
1486 .build();
1487
1488 let spec = MutationSpec::AddItem {
1489 target: MutationTargetSymbol::ByPath(Box::new(
1490 SymbolPath::parse("test_crate").unwrap(),
1491 )),
1492 content: "pub struct Container<T> { value: T }".to_string(),
1493 position: super::super::spec::InsertPosition::Bottom,
1494 };
1495
1496 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1497 let executor = BlueprintExecutor::new();
1498 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1499
1500 assert!(
1501 exec_result.success,
1502 "AddItem generic struct failed: {:?}",
1503 exec_result.error
1504 );
1505
1506 let file = ctx.test_file("src/lib.rs").unwrap();
1507 let source = file.to_source().unwrap();
1508 assert!(
1509 source.contains("struct Container"),
1510 "Generic struct not added: {}",
1511 source
1512 );
1513 }
1514
1515 #[test]
1516 fn test_blueprint_executor_add_item_async_fn() {
1517 let mut ctx = ContextBuilder::new()
1518 .with_file("src/lib.rs", "// empty file\n")
1519 .build();
1520
1521 let spec = MutationSpec::AddItem {
1522 target: MutationTargetSymbol::ByPath(Box::new(
1523 SymbolPath::parse("test_crate").unwrap(),
1524 )),
1525 content: "pub async fn fetch_data() -> String { String::new() }".to_string(),
1526 position: super::super::spec::InsertPosition::Bottom,
1527 };
1528
1529 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1530 let executor = BlueprintExecutor::new();
1531 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1532
1533 assert!(
1534 exec_result.success,
1535 "AddItem async fn failed: {:?}",
1536 exec_result.error
1537 );
1538
1539 let file = ctx.test_file("src/lib.rs").unwrap();
1540 let source = file.to_source().unwrap();
1541 assert!(
1542 source.contains("fn fetch_data"),
1543 "Async fn not added: {}",
1544 source
1545 );
1546 }
1547
1548 #[test]
1549 fn test_blueprint_executor_add_field_to_generic_struct() {
1550 use ryo_analysis::SymbolKind;
1551
1552 let mut ctx = ContextBuilder::new()
1553 .with_file("src/lib.rs", "pub struct Wrapper<T> { inner: T }\n")
1554 .build();
1555
1556 let symbol_id = ctx
1558 .registry
1559 .iter()
1560 .find(|(id, path)| {
1561 path.name() == "Wrapper" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
1562 })
1563 .map(|(id, _)| id)
1564 .expect("Wrapper struct not found in registry");
1565
1566 let spec = MutationSpec::AddField {
1567 target: MutationTargetSymbol::ById(symbol_id),
1568 field_name: "count".to_string(),
1569 field_type: "usize".to_string(),
1570 visibility: Visibility::Pub,
1571 };
1572
1573 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1574 let executor = BlueprintExecutor::new();
1575 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1576
1577 assert!(
1578 exec_result.success,
1579 "AddField to generic struct failed: {:?}",
1580 exec_result.error
1581 );
1582
1583 let file = ctx.test_file("src/lib.rs").unwrap();
1584 let source = file.to_source().unwrap();
1585 assert!(
1586 source.contains("pub count: usize"),
1587 "Field not added to generic struct: {}",
1588 source
1589 );
1590 }
1591
1592 #[test]
1593 fn test_blueprint_executor_add_derive_to_generic_struct() {
1594 let mut ctx = ContextBuilder::new()
1595 .with_file(
1596 "src/lib.rs",
1597 "pub struct Pair<T, U> { first: T, second: U }\n",
1598 )
1599 .build();
1600
1601 let path = SymbolPath::parse("test_crate::Pair").unwrap();
1603 let symbol_id = ctx.registry().lookup(&path).expect("Pair should exist");
1604
1605 let spec = MutationSpec::AddDerive {
1606 target: MutationTargetSymbol::ById(symbol_id),
1607 derives: vec!["Debug".to_string(), "Clone".to_string()],
1608 };
1609
1610 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1611 let executor = BlueprintExecutor::new();
1612 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1613
1614 assert!(
1615 exec_result.success,
1616 "AddDerive to generic struct failed: {:?}",
1617 exec_result.error
1618 );
1619
1620 let file = ctx.test_file("src/lib.rs").unwrap();
1621 let source = file.to_source().unwrap();
1622 assert!(
1623 source.contains("Debug"),
1624 "Debug derive not added: {}",
1625 source
1626 );
1627 assert!(
1628 source.contains("Clone"),
1629 "Clone derive not added: {}",
1630 source
1631 );
1632 }
1633
1634 #[test]
1639 fn test_blueprint_executor_create_mod_declaration() {
1640 let mut ctx = ContextBuilder::new()
1641 .with_file("src/lib.rs", "use std::io;\n\nfn main() {}\n")
1642 .build();
1643
1644 let spec = MutationSpec::CreateMod {
1645 target: MutationTargetSymbol::ByPath(Box::new(
1646 SymbolPath::parse("test_crate").unwrap(),
1647 )),
1648 mod_name: "models".to_string(),
1649 content: String::new(),
1650 is_pub: false,
1651 };
1652
1653 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1654 let executor = BlueprintExecutor::new();
1655 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1656
1657 assert!(
1658 exec_result.success,
1659 "CreateMod failed: {:?}",
1660 exec_result.error
1661 );
1662
1663 let file = ctx.test_file("src/lib.rs").unwrap();
1664 let source = file.to_source().unwrap();
1665 assert!(source.contains("mod models;"), "Mod not added: {}", source);
1666 }
1667
1668 #[test]
1669 fn test_blueprint_executor_create_pub_mod_declaration() {
1670 let mut ctx = ContextBuilder::new()
1671 .with_file("src/lib.rs", "fn main() {}\n")
1672 .build();
1673
1674 let spec = MutationSpec::CreateMod {
1675 target: MutationTargetSymbol::ByPath(Box::new(
1676 SymbolPath::parse("test_crate").unwrap(),
1677 )),
1678 mod_name: "api".to_string(),
1679 content: String::new(),
1680 is_pub: true,
1681 };
1682
1683 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1684 let executor = BlueprintExecutor::new();
1685 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1686
1687 assert!(
1688 exec_result.success,
1689 "CreateMod pub failed: {:?}",
1690 exec_result.error
1691 );
1692
1693 let file = ctx.test_file("src/lib.rs").unwrap();
1694 let source = file.to_source().unwrap();
1695 assert!(
1696 source.contains("pub mod api;"),
1697 "Pub mod not added: {}",
1698 source
1699 );
1700 }
1701
1702 #[test]
1703 fn test_blueprint_executor_create_file() {
1704 let mut ctx = ContextBuilder::new()
1706 .with_file("src/lib.rs", "// lib.rs\n")
1707 .build();
1708
1709 let spec = MutationSpec::CreateMod {
1710 target: MutationTargetSymbol::ByPath(Box::new(
1711 SymbolPath::parse("test_crate").unwrap(),
1712 )),
1713 mod_name: "models".to_string(),
1714 content: "pub struct Model { id: u32 }".to_string(),
1715 is_pub: true,
1716 };
1717
1718 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1719 let executor = BlueprintExecutor::new();
1720 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1721
1722 assert!(
1723 exec_result.success,
1724 "CreateFile failed: {:?}",
1725 exec_result.error
1726 );
1727
1728 assert!(ctx.test_file("src/models.rs").is_some(), "File not created");
1730
1731 let file = ctx.test_file("src/models.rs").unwrap();
1732 let source = file.to_source().unwrap();
1733 assert!(
1734 source.contains("struct Model"),
1735 "Content not correct: {}",
1736 source
1737 );
1738 }
1739
1740 #[test]
1741 fn test_blueprint_executor_create_module_workflow() {
1742 let mut ctx = ContextBuilder::new()
1743 .with_file("src/lib.rs", "fn main() {}\n")
1744 .build();
1745
1746 let spec = MutationSpec::CreateMod {
1748 target: MutationTargetSymbol::ByPath(Box::new(
1749 SymbolPath::parse("test_crate").unwrap(),
1750 )),
1751 mod_name: "utils".to_string(),
1752 content: "pub fn helper() {}".to_string(),
1753 is_pub: true,
1754 };
1755
1756 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1757 let executor = BlueprintExecutor::new();
1758 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1759
1760 assert!(
1761 exec_result.success,
1762 "Module workflow failed: {:?}",
1763 exec_result.error
1764 );
1765
1766 let lib = ctx.test_file("src/lib.rs").unwrap();
1768 let lib_source = lib.to_source().unwrap();
1769 assert!(
1770 lib_source.contains("pub mod utils;"),
1771 "Mod not added to lib: {}",
1772 lib_source
1773 );
1774
1775 let utils = ctx.test_file("src/utils.rs").unwrap();
1777 let utils_source = utils.to_source().unwrap();
1778 assert!(
1779 utils_source.contains("fn helper"),
1780 "Function not in utils: {}",
1781 utils_source
1782 );
1783 }
1784
1785 #[test]
1788 fn test_wavefront_execution_basic() {
1789 let mut ctx = create_test_context();
1790
1791 let path = SymbolPath::parse("test_crate::config::Config").unwrap();
1793 let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
1794
1795 let specs = vec![MutationSpec::AddDerive {
1796 target: MutationTargetSymbol::ById(symbol_id),
1797 derives: vec!["Debug".to_string()],
1798 }];
1799 let blueprint = ParallelBlueprint::from_mutations(specs);
1800
1801 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1802 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1803
1804 assert!(
1805 exec_result.success,
1806 "Wavefront basic failed: {:?}",
1807 exec_result.error
1808 );
1809 assert!(exec_result.total_changes > 0);
1810
1811 let file = ctx.test_file("src/config.rs").unwrap();
1812 let source = file.to_source().unwrap();
1813 assert!(source.contains("Debug"), "Derive not added: {}", source);
1814 }
1815
1816 #[test]
1817 fn test_wavefront_execution_multi_file() {
1818 let mut ctx = ContextBuilder::new()
1828 .with_file("src/lib.rs", "pub mod models;\n")
1829 .with_file("src/models.rs", "// models\n")
1830 .build();
1831
1832 let spec1 = MutationSpec::AddItem {
1834 target: MutationTargetSymbol::ByPath(Box::new(
1835 SymbolPath::parse("test_crate::models").unwrap(),
1836 )),
1837 content: "pub struct User { id: u32 }".to_string(),
1838 position: super::super::spec::InsertPosition::Bottom,
1839 };
1840
1841 let spec2 = MutationSpec::AddItem {
1842 target: MutationTargetSymbol::ByPath(Box::new(
1843 SymbolPath::parse("test_crate").unwrap(),
1844 )),
1845 content: "fn main() {}".to_string(),
1846 position: super::super::spec::InsertPosition::Bottom,
1847 };
1848
1849 let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
1850 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1851 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1852
1853 assert!(
1854 exec_result.success,
1855 "Wavefront multi-file failed: {:?}",
1856 exec_result.error
1857 );
1858
1859 let models = ctx.test_file("src/models.rs").unwrap();
1860 assert!(
1861 models.to_source().unwrap().contains("pub struct User"),
1862 "User not added to models.rs"
1863 );
1864
1865 let lib = ctx.test_file("src/lib.rs").unwrap();
1866 assert!(
1867 lib.to_source().unwrap().contains("fn main"),
1868 "main not added to lib.rs"
1869 );
1870 }
1871
1872 #[test]
1873 fn test_wavefront_execution_with_dependencies() {
1874 let mut ctx = ContextBuilder::new()
1875 .with_file("src/lib.rs", "fn main() {}\n")
1876 .build();
1877
1878 let spec = MutationSpec::CreateMod {
1880 target: MutationTargetSymbol::ByPath(Box::new(
1881 SymbolPath::parse("test_crate").unwrap(),
1882 )),
1883 mod_name: "api".to_string(),
1884 content: "pub fn endpoint() {}".to_string(),
1885 is_pub: true,
1886 };
1887
1888 let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
1889 let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1890 let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
1891
1892 assert!(
1893 exec_result.success,
1894 "Wavefront with deps failed: {:?}",
1895 exec_result.error
1896 );
1897
1898 let lib = ctx.test_file("src/lib.rs").unwrap();
1900 assert!(
1901 lib.to_source().unwrap().contains("pub mod api;"),
1902 "Mod not added to lib"
1903 );
1904
1905 let api = ctx.test_file("src/api.rs").unwrap();
1907 assert!(
1908 api.to_source().unwrap().contains("fn endpoint"),
1909 "Function not in api"
1910 );
1911 }
1912
1913 #[test]
1914 fn test_suggest_strategy() {
1915 let specs = vec![MutationSpec::AddDerive {
1917 target: MutationTargetSymbol::ById(dummy_id(1)),
1918 derives: vec!["Debug".to_string()],
1919 }];
1920 let blueprint = ParallelBlueprint::from_mutations(specs);
1921 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
1922
1923 let specs = vec![
1925 MutationSpec::AddDerive {
1926 target: MutationTargetSymbol::ById(dummy_id(1)),
1927 derives: vec!["Debug".to_string()],
1928 },
1929 MutationSpec::AddDerive {
1930 target: MutationTargetSymbol::ById(dummy_id(1)),
1931 derives: vec!["Clone".to_string()],
1932 },
1933 ];
1934 let blueprint = ParallelBlueprint::from_mutations(specs);
1935 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
1936
1937 let specs = vec![
1939 MutationSpec::AddDerive {
1940 target: MutationTargetSymbol::ById(dummy_id(1)),
1941 derives: vec!["Debug".to_string()],
1942 },
1943 MutationSpec::AddDerive {
1944 target: MutationTargetSymbol::ById(dummy_id(1)),
1945 derives: vec!["Clone".to_string()],
1946 },
1947 MutationSpec::AddDerive {
1948 target: MutationTargetSymbol::ById(dummy_id(1)),
1949 derives: vec!["Default".to_string()],
1950 },
1951 MutationSpec::AddDerive {
1952 target: MutationTargetSymbol::ById(dummy_id(1)),
1953 derives: vec!["Hash".to_string()],
1954 },
1955 ];
1956 let blueprint = ParallelBlueprint::from_mutations(specs);
1957 assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Wavefront);
1958 }
1959
1960 #[test]
1961 #[ignore = "V1 path disabled - needs V2 migration"]
1962 fn test_sequential_and_wavefront_produce_same_result() {
1963 let code = r#"
1964struct A {}
1965struct B {}
1966struct C {}
1967"#;
1968
1969 let specs = vec![
1970 MutationSpec::AddDerive {
1971 target: MutationTargetSymbol::ById(dummy_id(1)),
1972 derives: vec!["Debug".to_string()],
1973 },
1974 MutationSpec::AddDerive {
1975 target: MutationTargetSymbol::ById(dummy_id(1)),
1976 derives: vec!["Clone".to_string()],
1977 },
1978 MutationSpec::AddDerive {
1979 target: MutationTargetSymbol::ById(dummy_id(1)),
1980 derives: vec!["Default".to_string()],
1981 },
1982 ];
1983 let blueprint = ParallelBlueprint::from_mutations(specs.clone());
1984
1985 let mut ctx_seq = ContextBuilder::new().with_file("src/lib.rs", code).build();
1987 let executor_seq = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Sequential);
1988 let result_seq = execute_and_sync(&executor_seq, &blueprint, &mut ctx_seq);
1989
1990 let mut ctx_wave = ContextBuilder::new().with_file("src/lib.rs", code).build();
1992 let executor_wave = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
1993 let result_wave = execute_and_sync(&executor_wave, &blueprint, &mut ctx_wave);
1994
1995 assert!(result_seq.success, "Sequential failed");
1997 assert!(result_wave.success, "Wavefront failed");
1998
1999 let source_seq = ctx_seq
2001 .test_file("src/lib.rs")
2002 .unwrap()
2003 .to_source()
2004 .unwrap();
2005 let source_wave = ctx_wave
2006 .test_file("src/lib.rs")
2007 .unwrap()
2008 .to_source()
2009 .unwrap();
2010
2011 assert!(source_seq.contains("Debug"), "Sequential missing Debug");
2012 assert!(source_seq.contains("Clone"), "Sequential missing Clone");
2013 assert!(source_seq.contains("Default"), "Sequential missing Default");
2014 assert!(source_wave.contains("Debug"), "Wavefront missing Debug");
2015 assert!(source_wave.contains("Clone"), "Wavefront missing Clone");
2016 assert!(source_wave.contains("Default"), "Wavefront missing Default");
2017 }
2018
2019 #[test]
2020 #[ignore = "flaky: µs-level timing comparison depends on CPU load"]
2021 fn test_execute_v2_without_sync_is_faster() {
2022 use ryo_analysis::SymbolKind;
2023 use std::time::Instant;
2024
2025 let code = r#"
2027pub struct Config { name: String, value: i32 }
2028pub struct User { id: u64, name: String, email: String }
2029pub struct Order { id: u64, user_id: u64, total: f64 }
2030pub enum Status { Pending, Active, Completed, Failed }
2031pub trait Processor { fn process(&self); }
2032impl Processor for Config { fn process(&self) {} }
2033impl Processor for User { fn process(&self) {} }
2034"#;
2035
2036 fn create_specs(ctx: &ryo_analysis::AnalysisContext) -> Vec<MutationSpec> {
2038 let config_id = ctx
2039 .registry
2040 .iter()
2041 .find(|(id, path)| {
2042 path.name() == "Config" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2043 })
2044 .map(|(id, _)| id)
2045 .expect("Config not found");
2046
2047 let user_id = ctx
2048 .registry
2049 .iter()
2050 .find(|(id, path)| {
2051 path.name() == "User" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
2052 })
2053 .map(|(id, _)| id)
2054 .expect("User not found");
2055
2056 vec![
2057 MutationSpec::AddField {
2058 target: MutationTargetSymbol::ById(config_id),
2059 field_name: "enabled".to_string(),
2060 field_type: "bool".to_string(),
2061 visibility: Visibility::Pub,
2062 },
2063 MutationSpec::AddDerive {
2064 target: MutationTargetSymbol::ById(user_id),
2065 derives: vec!["Debug".to_string(), "Clone".to_string()],
2066 },
2067 ]
2068 }
2069
2070 let iterations = 10;
2072 let mut execute_only_times = Vec::with_capacity(iterations);
2073
2074 for _ in 0..iterations {
2075 let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
2076 let specs = create_specs(&ctx);
2077 let blueprint = ParallelBlueprint::from_mutations(specs);
2078 let executor = BlueprintExecutor::new();
2079
2080 let t0 = Instant::now();
2081 let result = executor.execute_v2(&blueprint, &mut ctx);
2082 execute_only_times.push(t0.elapsed());
2083
2084 assert!(result.success);
2085 }
2086
2087 let mut execute_with_sync_times = Vec::with_capacity(iterations);
2089
2090 for _ in 0..iterations {
2091 let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
2092 let specs = create_specs(&ctx);
2093 let blueprint = ParallelBlueprint::from_mutations(specs);
2094 let executor = BlueprintExecutor::new();
2095
2096 let t0 = Instant::now();
2097 let result = executor.execute_v2(&blueprint, &mut ctx);
2098 BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
2099 execute_with_sync_times.push(t0.elapsed());
2100
2101 assert!(result.success);
2102 }
2103
2104 let avg_execute_only: u128 = execute_only_times
2105 .iter()
2106 .map(|d| d.as_micros())
2107 .sum::<u128>()
2108 / iterations as u128;
2109 let avg_with_sync: u128 = execute_with_sync_times
2110 .iter()
2111 .map(|d| d.as_micros())
2112 .sum::<u128>()
2113 / iterations as u128;
2114
2115 eprintln!(
2116 "execute_v2 only: {}µs avg, execute_v2 + sync: {}µs avg, sync overhead: {:.1}x",
2117 avg_execute_only,
2118 avg_with_sync,
2119 avg_with_sync as f64 / avg_execute_only as f64
2120 );
2121
2122 assert!(
2124 avg_execute_only < avg_with_sync,
2125 "execute_v2 only ({}µs) should be faster than with sync ({}µs)",
2126 avg_execute_only,
2127 avg_with_sync
2128 );
2129 }
2130
2131 use ryo_symbol::{CrateLayout, CrateName, WorkspaceFilePath, WorkspaceMetadataProvider};
2147 use std::path::{Path, PathBuf};
2148
2149 struct FixedLayoutProvider {
2154 layout: CrateLayout,
2155 }
2156
2157 impl WorkspaceMetadataProvider for FixedLayoutProvider {
2158 fn crate_for_file(&self, _path: &WorkspaceFilePath) -> Option<CrateName> {
2159 unreachable!("ws_file_to_crate_relative_key does not call crate_for_file");
2160 }
2161 fn all_crates(&self) -> Vec<CrateName> {
2162 vec![]
2163 }
2164 fn crate_root(&self, _crate_name: &CrateName) -> Option<PathBuf> {
2165 None
2166 }
2167 fn workspace_root(&self) -> &Path {
2168 Path::new("/workspace")
2169 }
2170 fn crate_layout(&self, _crate_name: &CrateName) -> Option<CrateLayout> {
2171 Some(self.layout.clone())
2172 }
2173 }
2174
2175 #[test]
2176 fn ws_key_root_layout_flat_file() {
2177 let wfp = WorkspaceFilePath::new_for_test("src/storage.rs", "/workspace", "test_crate");
2178 let provider = FixedLayoutProvider {
2179 layout: CrateLayout::Root,
2180 };
2181 assert_eq!(
2182 ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2183 Some("src/storage.rs")
2184 );
2185 }
2186
2187 #[test]
2188 fn ws_key_in_crates_hyphen_name_flat_file() {
2189 let wfp = WorkspaceFilePath::new_for_test(
2195 "crates/ryo-app/src/storage.rs",
2196 "/workspace",
2197 "ryo_app",
2198 );
2199 let provider = FixedLayoutProvider {
2200 layout: CrateLayout::InCrates {
2201 crate_dir_name: "ryo-app".to_string(),
2202 },
2203 };
2204 assert_eq!(
2205 ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2206 Some("src/storage.rs"),
2207 "Hyphen-named InCrates layout must strip 'crates/ryo-app/' prefix \
2208 so the generator's with_existing_files sees the flat src/storage.rs"
2209 );
2210 }
2211
2212 #[test]
2213 fn ws_key_in_crates_hyphen_name_nested_module() {
2214 let wfp = WorkspaceFilePath::new_for_test(
2215 "crates/ryo-app/src/discover/service.rs",
2216 "/workspace",
2217 "ryo_app",
2218 );
2219 let provider = FixedLayoutProvider {
2220 layout: CrateLayout::InCrates {
2221 crate_dir_name: "ryo-app".to_string(),
2222 },
2223 };
2224 assert_eq!(
2225 ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2226 Some("src/discover/service.rs")
2227 );
2228 }
2229
2230 #[test]
2231 fn ws_key_custom_prefix_layout() {
2232 let wfp = WorkspaceFilePath::new_for_test("packages/core/src/lib.rs", "/workspace", "core");
2233 let provider = FixedLayoutProvider {
2234 layout: CrateLayout::Custom {
2235 prefix: PathBuf::from("packages/core"),
2236 },
2237 };
2238 assert_eq!(
2239 ws_file_to_crate_relative_key(&wfp, &provider).as_deref(),
2240 Some("src/lib.rs")
2241 );
2242 }
2243
2244 #[test]
2245 fn ws_key_layout_lookup_failure_returns_none() {
2246 struct NoLayoutProvider;
2248 impl WorkspaceMetadataProvider for NoLayoutProvider {
2249 fn crate_for_file(&self, _p: &WorkspaceFilePath) -> Option<CrateName> {
2250 None
2251 }
2252 fn all_crates(&self) -> Vec<CrateName> {
2253 vec![]
2254 }
2255 fn crate_root(&self, _c: &CrateName) -> Option<PathBuf> {
2256 None
2257 }
2258 fn workspace_root(&self) -> &Path {
2259 Path::new("/workspace")
2260 }
2261 fn crate_layout(&self, _c: &CrateName) -> Option<CrateLayout> {
2262 None
2263 }
2264 }
2265 let wfp = WorkspaceFilePath::new_for_test(
2266 "crates/ryo-app/src/storage.rs",
2267 "/workspace",
2268 "ryo_app",
2269 );
2270 assert_eq!(ws_file_to_crate_relative_key(&wfp, &NoLayoutProvider), None);
2271 }
2272}