#[cfg(test)]
mod integration {
use ryo_analysis::{AnalysisContext, SymbolKind, SymbolPath};
use ryo_mutations::basic::{AddFieldMutation, CreateModMutation};
use ryo_source::pure::{PureField, PureFields, PureFile, PureItem, PureStruct, PureVis};
use ryo_symbol::WorkspaceFilePath;
use std::sync::Arc;
use crate::engine::{multi_file_dumper, ASTMutationEngine};
fn test_context() -> AnalysisContext {
let file_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/test", "crate");
let file = Arc::new(PureFile::new());
let mut files = im::HashMap::new();
files.insert(file_path, file);
AnalysisContext::from_im_files(files)
}
fn create_test_struct(name: &str) -> PureStruct {
PureStruct {
attrs: vec![],
vis: PureVis::Public,
name: name.to_string(),
generics: Default::default(),
fields: PureFields::Named(vec![]),
}
}
#[test]
fn test_add_field_to_registry() {
let mut ctx = test_context();
let path = SymbolPath::parse("test_crate::MyStruct").unwrap();
let id = ctx
.registry
.register(path.clone(), SymbolKind::Struct)
.unwrap();
let struct_ast = create_test_struct("MyStruct");
ctx.ast_registry.set(id, PureItem::Struct(struct_ast));
let mutation = AddFieldMutation::new(id, "new_field", "i32");
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes(), "Mutation should have made changes");
assert_eq!(result.result.changes, 1);
assert!(!result.events.is_empty(), "Should have emitted events");
let ast = ctx.ast_registry.get(id).expect("AST should exist");
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 1, "Should have one field");
assert_eq!(fields[0].name, "new_field");
} else {
panic!("Expected named fields");
}
} else {
panic!("Expected struct");
}
}
#[test]
fn test_create_mod_to_registry() {
let mut ctx = test_context();
let crate_path = SymbolPath::parse("test_crate").unwrap();
let crate_id = ctx
.registry
.register(crate_path.clone(), SymbolKind::Mod)
.unwrap();
let mutation = CreateModMutation::new(crate_id, "my_module").public();
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes(), "Mutation should have made changes");
assert_eq!(result.result.changes, 1);
let path = SymbolPath::parse("test_crate::my_module").unwrap();
let id = ctx.registry.lookup(&path);
assert!(id.is_some(), "Module should be registered");
let id = id.unwrap();
assert_eq!(
ctx.registry.kind(id),
Some(SymbolKind::Mod),
"Should be Mod kind"
);
assert!(
ctx.ast_registry.get(id).is_none(),
"Mod declarations should not be in ASTRegistry"
);
}
#[test]
fn test_file_dumper_output() {
let mut ctx = test_context();
let path = SymbolPath::parse("test_crate::TestStruct").unwrap();
let id = ctx
.registry
.register(path.clone(), SymbolKind::Struct)
.unwrap();
let struct_ast = PureStruct {
attrs: vec![],
vis: PureVis::Public,
name: "TestStruct".to_string(),
generics: Default::default(),
fields: PureFields::Named(vec![PureField {
attrs: vec![],
vis: PureVis::Public,
name: "value".to_string(),
ty: ryo_source::pure::PureType::Path("i32".to_string()),
}]),
};
ctx.ast_registry.set(id, PureItem::Struct(struct_ast));
let file_path = WorkspaceFilePath::new_for_test("src/lib.rs", "/test", "crate");
let _ = ctx.registry.set_span(
id,
ryo_symbol::FileSpan {
file: file_path.clone(),
start: 0,
end: 100,
},
);
let files = multi_file_dumper().dump_all(&ctx).unwrap();
assert!(!files.is_empty(), "Should have generated files");
if let Some(content) = files.get(&file_path) {
assert!(content.contains("TestStruct"), "Should contain struct name");
assert!(content.contains("value"), "Should contain field name");
assert!(content.contains("i32"), "Should contain field type");
} else {
println!("Note: File not found in dump output (expected in minimal test)");
}
}
#[test]
fn test_full_pipeline_add_field() {
let mut ctx = test_context();
let path = SymbolPath::parse("test_crate::User").unwrap();
let id = ctx.registry.register(path, SymbolKind::Struct).unwrap();
let struct_ast = create_test_struct("User");
ctx.ast_registry.set(id, PureItem::Struct(struct_ast));
let mutation = AddFieldMutation::new(id, "name", "String").public();
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes());
let ast = ctx.ast_registry.get(id).unwrap();
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name, "name");
assert!(matches!(fields[0].vis, PureVis::Public));
}
}
}
mod context_builder_tests {
use super::*;
use ryo_analysis::testing::ContextBuilder;
use ryo_mutations::basic::RemoveFieldMutation;
#[test]
fn test_ast_registry_populated_from_file() {
let ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Config { name: String }")
.build();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let id = ctx
.registry()
.lookup(&path)
.expect("Config should be registered");
let ast = ctx.ast_registry.get(id).expect("AST should exist");
if let PureItem::Struct(s) = ast {
assert_eq!(s.name, "Config");
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name, "name");
}
} else {
panic!("Expected struct item");
}
}
#[test]
fn test_add_field_via_context_builder() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct User { id: u32 }")
.build();
let path = SymbolPath::parse("test_crate::User").unwrap();
let id = ctx.registry().lookup(&path).expect("User should exist");
let ast = ctx.ast_registry.get(id).expect("AST should exist");
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 1, "Should have 1 field initially");
}
}
let mutation = AddFieldMutation::new(id, "name", "String").public();
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes(), "Mutation should succeed");
assert_eq!(result.result.changes, 1);
let ast = ctx.ast_registry.get(id).unwrap();
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 2, "Should have 2 fields after mutation");
assert_eq!(fields[1].name, "name");
}
}
let files = multi_file_dumper().dump_all(&ctx).unwrap();
let lib_path =
ryo_symbol::WorkspaceFilePath::new_for_test("src/lib.rs", "/test", "crate");
if let Some(content) = files.get(&lib_path) {
assert!(content.contains("User"), "Should contain struct name");
assert!(content.contains("name"), "Should contain new field");
assert!(content.contains("String"), "Should contain field type");
}
}
#[test]
fn test_remove_field_via_context_builder() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
"pub struct Config { name: String, value: i32 }",
)
.build();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let id = ctx.registry().lookup(&path).expect("Config should exist");
let mutation = RemoveFieldMutation::new(id, "name");
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes());
let ast = ctx.ast_registry.get(id).unwrap();
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 1, "Should have 1 field after removal");
assert_eq!(fields[0].name, "value", "Remaining field should be 'value'");
}
}
}
#[test]
fn test_multiple_mutations_sequence() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Entity {}")
.build();
let path = SymbolPath::parse("test_crate::Entity").unwrap();
let id = ctx.registry().lookup(&path).unwrap();
let mutations = vec![
AddFieldMutation::new(id, "id", "u64").public(),
AddFieldMutation::new(id, "name", "String").public(),
AddFieldMutation::new(id, "created_at", "i64"),
];
for mutation in &mutations {
let result = ASTMutationEngine::execute_ast_reg(mutation, &mut ctx);
assert!(result.has_changes());
}
let ast = ctx.ast_registry.get(id).unwrap();
if let PureItem::Struct(s) = ast {
if let PureFields::Named(fields) = &s.fields {
assert_eq!(fields.len(), 3);
assert_eq!(fields[0].name, "id");
assert_eq!(fields[1].name, "name");
assert_eq!(fields[2].name, "created_at");
}
}
}
#[test]
fn test_create_mod_creates_module() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs")
.build();
let crate_path = SymbolPath::parse("test_crate").unwrap();
let crate_id = ctx
.registry
.lookup(&crate_path)
.expect("crate module should exist");
let mutation = CreateModMutation::new(crate_id, "models").public();
let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result.has_changes());
let path = SymbolPath::parse("test_crate::models").unwrap();
let id = ctx.registry().lookup(&path);
assert!(id.is_some(), "Module should be registered");
let id = id.unwrap();
assert_eq!(
ctx.registry().kind(id),
Some(SymbolKind::Mod),
"Should be Mod kind"
);
assert!(
ctx.ast_registry.get(id).is_none(),
"Mod declarations should not be in ASTRegistry"
);
}
#[test]
fn test_add_field_idempotent() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Data {}")
.build();
let path = SymbolPath::parse("test_crate::Data").unwrap();
let id = ctx.registry().lookup(&path).unwrap();
let mutation = AddFieldMutation::new(id, "value", "i32");
let result1 = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(result1.has_changes());
let result2 = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
assert!(
!result2.has_changes(),
"Adding same field twice should not change anything"
);
}
}
mod blueprint_v2_tests {
use crate::executor::{
BlueprintExecutor, MutationSpec, MutationTargetSymbol, ParallelBlueprint, Visibility,
};
use ryo_analysis::testing::ContextBuilder;
use ryo_analysis::SymbolPath;
#[test]
fn test_execute_v2_add_field() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Config { name: String }")
.build();
let path = SymbolPath::parse("test_crate::Config").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
let spec = MutationSpec::AddField {
target: MutationTargetSymbol::ById(symbol_id),
field_name: "value".to_string(),
field_type: "i32".to_string(),
visibility: Visibility::Pub,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(
result.success,
"execute_v2 should succeed: {:?}",
result.error
);
assert_eq!(result.results.len(), 1);
assert!(result.results[0].success);
let _ = BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx);
let file = ctx.files().iter().next().unwrap().1;
let source = file.to_source().unwrap();
assert!(
source.contains("value"),
"Output should contain new field: {}",
source
);
}
#[test]
fn test_execute_v2_remove_field() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct User { id: u32, name: String }")
.build();
let path = SymbolPath::parse("test_crate::User").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("User should exist");
let spec = MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(symbol_id),
field_name: "name".to_string(),
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(result.success);
let _ = BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx);
let file = ctx.files().iter().next().unwrap().1;
let source = file.to_source().unwrap();
assert!(source.contains("id"), "Should still have id field");
assert!(
!source.contains("name"),
"Should not have name field: {}",
source
);
}
#[test]
fn test_execute_v2_create_mod() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "models".to_string(),
content: String::new(),
is_pub: true,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(
result.success,
"CreateMod should succeed: {:?}",
result.error
);
let path = SymbolPath::parse("test_crate::models").unwrap();
assert!(
ctx.registry().lookup(&path).is_some(),
"Module should be registered"
);
}
#[test]
fn test_execute_v2_multiple_specs() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Entity {}")
.build();
let path = SymbolPath::parse("test_crate::Entity").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("Entity should exist");
let specs = vec![
MutationSpec::AddField {
target: MutationTargetSymbol::ById(symbol_id),
field_name: "id".to_string(),
field_type: "u64".to_string(),
visibility: Visibility::Pub,
},
MutationSpec::AddField {
target: MutationTargetSymbol::ById(symbol_id),
field_name: "name".to_string(),
field_type: "String".to_string(),
visibility: Visibility::Pub,
},
];
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(result.success);
assert_eq!(result.results.len(), 2);
let _ = BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx);
let file = ctx.files().iter().next().unwrap().1;
let source = file.to_source().unwrap();
assert!(source.contains("id"), "Should have id field: {}", source);
assert!(
source.contains("name"),
"Should have name field: {}",
source
);
}
}
}