use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
use ryo_analysis::{AnalysisContext, SymbolId};
use std::collections::HashMap;
use std::path::PathBuf;
use ryo_executor::{
BlueprintExecutor, InsertPosition, ItemKind, MutationSpec, MutationTargetSymbol,
ParallelBlueprint, Scope, SelfParam, StmtInsertPosition, VariantKind, Visibility,
};
use ryo_symbol::SymbolPath;
use ryo_test::harness::{normalize_source, sources_equal};
fn crate_root() -> SymbolPath {
SymbolPath::parse("test_crate").expect("crate is a valid SymbolPath")
}
fn execute_mutation(input: &str, specs: Vec<MutationSpec>) -> String {
let mut ctx = ContextBuilder::new().with_file("src/lib.rs", input).build();
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::default();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(result.success, "Mutation failed: {:?}", result.error);
BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
ctx.test_file("src/lib.rs")
.map(|f| f.to_source().unwrap())
.unwrap_or_default()
}
fn execute_mutation_v2(input: &str, specs: Vec<MutationSpec>) -> String {
let mut ctx = ContextBuilder::new().with_file("src/lib.rs", input).build();
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::default();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(result.success, "V2 Mutation failed: {:?}", result.error);
BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
ctx.test_file("src/lib.rs")
.map(|f| f.to_source().unwrap())
.unwrap_or_default()
}
fn execute_mutation_with_symbol_id<F>(input: &str, spec_builder: F) -> String
where
F: FnOnce(&AnalysisContext) -> Vec<MutationSpec>,
{
let mut ctx = ContextBuilder::new().with_file("src/lib.rs", input).build();
let specs = spec_builder(&ctx);
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::default();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(
result.success,
"Mutation with SymbolId failed: {:?}",
result.error
);
BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
ctx.test_file("src/lib.rs")
.map(|f| f.to_source().unwrap())
.unwrap_or_default()
}
fn lookup_symbol_id(ctx: &AnalysisContext, name: &str) -> SymbolId {
ctx.registry()
.lookup_by_name(name)
.unwrap_or_else(|| panic!("Symbol '{}' not found in registry", name))
}
fn dummy_id(index: u32) -> SymbolId {
SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
}
fn execute_mutation_v2_multi(
files: &[(&str, &str)],
specs: Vec<MutationSpec>,
) -> HashMap<PathBuf, String> {
let mut builder = ContextBuilder::new();
for (path, content) in files {
builder = builder.with_file(path, content);
}
let mut ctx = builder.build();
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::default();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(
result.success,
"V2 Multi-file mutation failed: {:?}",
result.error
);
BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
let mut output = HashMap::new();
for (path, _) in files {
if let Some(file) = ctx.test_file(path) {
output.insert(PathBuf::from(path), file.to_source().unwrap());
}
}
output
}
#[test]
fn test_add_field_to_struct() {
let input = r#"
pub struct User {
pub name: String,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "User");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(id),
field_name: "age".into(),
field_type: "u32".into(),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub age: u32"),
"Field not added: {}",
output
);
assert!(output.contains("pub name: String"), "Original field lost");
}
#[test]
fn test_add_private_field() {
let input = "pub struct Config { pub debug: bool }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Config");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(id),
field_name: "internal".into(),
field_type: "Vec<u8>".into(),
visibility: Visibility::Private,
}]
});
assert!(
output.contains("internal: Vec<u8>"),
"Private field not added: {}",
output
);
}
#[test]
fn test_add_derive_single() {
let input = r#"
#[derive(Debug)]
pub struct Item {
pub id: u32,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Item");
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id),
derives: vec!["Clone".into()],
}]
});
assert!(
output.contains("Clone"),
"Clone derive not added: {}",
output
);
assert!(output.contains("Debug"), "Original derive lost");
}
#[test]
fn test_add_derive_multiple() {
let input = "pub struct Data {}";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Data");
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id),
derives: vec!["Debug".into(), "Clone".into(), "PartialEq".into()],
}]
});
assert!(output.contains("Debug"), "Debug not added");
assert!(output.contains("Clone"), "Clone not added");
assert!(output.contains("PartialEq"), "PartialEq not added");
}
#[test]
fn test_add_variant_to_enum() {
let input = r#"
pub enum Status {
Active,
Inactive,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Status");
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(id),
variant_name: "Pending".into(),
variant_kind: VariantKind::Unit,
}]
});
assert!(output.contains("Pending"), "Variant not added: {}", output);
assert!(output.contains("Active"), "Original variant lost");
assert!(output.contains("Inactive"), "Original variant lost");
}
#[test]
fn test_add_variant_with_fields() {
let input = "pub enum Event { Click }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Event");
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(id),
variant_name: "KeyPress".into(),
variant_kind: VariantKind::Struct {
fields: vec![
("key".into(), "char".into()),
("modifiers".into(), "u8".into()),
],
},
}]
});
assert!(output.contains("KeyPress"), "Variant not added: {}", output);
assert!(
output.contains("key: char"),
"Variant fields not added: {}",
output
);
}
#[test]
fn test_multiple_independent_mutations() {
let input = r#"
pub struct A {}
pub struct B {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id_a = lookup_symbol_id(ctx, "A");
let id_b = lookup_symbol_id(ctx, "B");
vec![
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id_a),
derives: vec!["Debug".into()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id_b),
derives: vec!["Clone".into()],
},
]
});
assert!(output.contains("Debug"), "A should have Debug");
assert!(output.contains("Clone"), "B should have Clone");
}
#[test]
fn test_add_field_to_nonexistent_struct() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Other {}")
.build();
let blueprint = ParallelBlueprint::from_mutations(vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(999)),
field_name: "field".into(),
field_type: "i32".into(),
visibility: Visibility::Pub,
}]);
let executor = BlueprintExecutor::default();
let result = executor.execute_v2(&blueprint, &mut ctx);
assert!(!result.success || result.total_changes == 0);
}
#[test]
fn test_source_normalization() {
let messy = " pub struct Foo { \n bar: i32,\n } ";
let clean = "pub struct Foo {\nbar: i32,\n}";
assert_eq!(normalize_source(messy), clean);
}
#[test]
fn test_sources_equal_ignores_whitespace() {
let a = "pub struct Foo {\nbar: i32\n}";
let b = "pub struct Foo {\n bar: i32\n}";
assert!(sources_equal(a, b));
}
#[test]
fn test_remove_field_from_struct() {
let input = r#"
pub struct User {
pub name: String,
pub age: u32,
pub email: String,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "User");
vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(id),
field_name: "age".into(),
}]
});
assert!(!output.contains("age"), "Field not removed: {}", output);
assert!(output.contains("name"), "Other field lost");
assert!(output.contains("email"), "Other field lost");
}
#[test]
fn test_remove_derive() {
let input = r#"
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub id: u32,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Item");
vec![MutationSpec::RemoveDerive {
target: MutationTargetSymbol::ById(id),
derives: vec!["Clone".into()],
}]
});
assert!(!output.contains("Clone"), "Clone not removed: {}", output);
assert!(output.contains("Debug"), "Other derive lost");
assert!(output.contains("PartialEq"), "Other derive lost");
}
#[test]
fn test_remove_variant() {
let input = r#"
pub enum Status {
Active,
Pending,
Inactive,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Status");
vec![MutationSpec::RemoveVariant {
target: MutationTargetSymbol::ById(id),
variant_name: "Pending".into(),
}]
});
assert!(
!output.contains("Pending"),
"Variant not removed: {}",
output
);
assert!(output.contains("Active"), "Other variant lost");
assert!(output.contains("Inactive"), "Other variant lost");
}
#[test]
fn test_rename_struct() {
let input = "pub struct OldName { pub value: i32 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "OldName");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "NewName".into(),
scope: Scope::Project,
}]
});
assert!(output.contains("NewName"), "Struct not renamed: {}", output);
assert!(!output.contains("OldName"), "Old name still present");
}
#[test]
fn test_rename_function() {
let input = "pub fn old_function() -> i32 { 42 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "old_function");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "new_function".into(),
scope: Scope::Project,
}]
});
assert!(
output.contains("new_function"),
"Function not renamed: {}",
output
);
assert!(!output.contains("old_function"), "Old name still present");
}
#[test]
fn test_rename_enum() {
let input = "pub enum OldEnum { A, B }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "OldEnum");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "NewEnum".into(),
scope: Scope::Project,
}]
});
assert!(output.contains("NewEnum"), "Enum not renamed: {}", output);
assert!(!output.contains("OldEnum"), "Old name still present");
}
#[test]
fn test_change_visibility_private_to_pub() {
let input = "struct Private { field: i32 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Private");
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(id),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub struct Private"),
"Visibility not changed: {}",
output
);
}
#[test]
fn test_change_visibility_pub_to_pub_crate() {
let input = "pub struct Public { field: i32 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Public");
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(id),
visibility: Visibility::PubCrate,
}]
});
assert!(
output.contains("pub(crate)"),
"Visibility not changed to pub(crate): {}",
output
);
}
#[test]
fn test_change_visibility_function() {
let input = "fn private_fn() {}";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "private_fn");
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(id),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub fn private_fn"),
"Function visibility not changed: {}",
output
);
}
#[test]
fn test_add_variant_tuple() {
let input = "pub enum Message { Text(String) }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Message");
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(id),
variant_name: "Binary".into(),
variant_kind: VariantKind::Tuple {
types: vec!["Vec<u8>".into()],
},
}]
});
assert!(output.contains("Binary"), "Variant not added: {}", output);
assert!(
output.contains("Vec<u8>"),
"Tuple type not added: {}",
output
);
}
#[test]
fn test_add_field_with_generic_type() {
let input = "pub struct Container<T> { pub items: Vec<T> }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Container");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(id),
field_name: "capacity".into(),
field_type: "usize".into(),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub capacity: usize"),
"Field not added: {}",
output
);
assert!(output.contains("Vec<T>"), "Generic preserved");
}
#[test]
fn test_add_derive_to_enum() {
let input = "pub enum Color { Red, Green, Blue }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Color");
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id),
derives: vec!["Debug".into(), "Clone".into(), "Copy".into()],
}]
});
assert!(output.contains("Debug"), "Debug not added");
assert!(output.contains("Clone"), "Clone not added");
assert!(output.contains("Copy"), "Copy not added");
}
#[test]
fn test_add_derive_no_existing_derives() {
let input = "pub struct Plain { pub value: i32 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Plain");
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(id),
derives: vec!["Debug".into()],
}]
});
assert!(
output.contains("#[derive(Debug)]"),
"Derive attribute not added: {}",
output
);
}
#[test]
fn test_add_field_to_empty_struct() {
let input = "pub struct Empty {}";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Empty");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(id),
field_name: "id".into(),
field_type: "u64".into(),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub id: u64"),
"Field not added to empty struct: {}",
output
);
}
#[test]
fn test_add_variant_to_empty_enum() {
let input = "pub enum Empty {}";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Empty");
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(id),
variant_name: "First".into(),
variant_kind: VariantKind::Unit,
}]
});
assert!(
output.contains("First"),
"Variant not added to empty enum: {}",
output
);
}
#[test]
fn test_add_item_struct() {
let input = "pub struct Existing {}";
let output = execute_mutation(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "pub struct NewStruct { pub id: u64 }".into(),
position: InsertPosition::default(),
}],
);
assert!(output.contains("NewStruct"), "Struct not added: {}", output);
assert!(output.contains("Existing"), "Original struct lost");
}
#[test]
fn test_add_item_function() {
let input = "pub fn existing() {}";
let output = execute_mutation(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "pub fn new_function() -> i32 { 42 }".into(),
position: InsertPosition::default(),
}],
);
assert!(
output.contains("new_function"),
"Function not added: {}",
output
);
assert!(output.contains("existing"), "Original function lost");
}
#[test]
fn test_add_item_impl_block() {
let input = "pub struct Data { pub value: i32 }";
let output = execute_mutation(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "impl Data { pub fn new(value: i32) -> Self { Self { value } } }".into(),
position: InsertPosition::default(),
}],
);
assert!(
output.contains("impl Data"),
"Impl block not added: {}",
output
);
assert!(output.contains("fn new"), "Method not in impl: {}", output);
}
#[test]
fn test_add_item_enum() {
let input = "";
let output = execute_mutation(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "#[derive(Debug)]\npub enum Status { Active, Inactive }".into(),
position: InsertPosition::default(),
}],
);
assert!(output.contains("enum Status"), "Enum not added: {}", output);
assert!(output.contains("Active"), "Variant missing");
assert!(output.contains("#[derive(Debug)]"), "Derive missing");
}
#[test]
fn test_add_method_to_impl() {
let input = r#"
pub struct Counter {
value: i32,
}
impl Counter {
pub fn new() -> Self {
Self { value: 0 }
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let counter_id = lookup_symbol_id(ctx, "Counter");
vec![MutationSpec::AddMethod {
target: MutationTargetSymbol::ById(counter_id),
method_name: "increment".into(),
params: vec![],
return_type: None,
body: "self.value += 1".into(),
is_pub: true,
self_param: Some(SelfParam::Mut),
}]
});
assert!(output.contains("increment"), "Method not added: {}", output);
assert!(output.contains("self.value += 1"), "Method body incorrect");
}
#[test]
fn test_add_method_with_return_type() {
let input = r#"
pub struct Config {
debug: bool,
}
impl Config {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let config_id = lookup_symbol_id(ctx, "Config");
vec![MutationSpec::AddMethod {
target: MutationTargetSymbol::ById(config_id),
method_name: "is_debug".into(),
params: vec![],
return_type: Some("bool".into()),
body: "self.debug".into(),
is_pub: true,
self_param: Some(SelfParam::Ref),
}]
});
assert!(
output.contains("fn is_debug"),
"Method not added: {}",
output
);
assert!(output.contains("-> bool"), "Return type missing");
}
#[test]
fn test_remove_item_function() {
let input = r#"
pub fn keep_me() {}
pub fn remove_me() {}
pub fn also_keep() {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "remove_me");
vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(id),
item_kind: ItemKind::Function,
}]
});
assert!(
!output.contains("remove_me"),
"Function not removed: {}",
output
);
assert!(output.contains("keep_me"), "Other function lost");
assert!(output.contains("also_keep"), "Other function lost");
}
#[test]
fn test_remove_item_struct() {
let input = r#"
pub struct Keep {}
pub struct Remove {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Remove");
vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(id),
item_kind: ItemKind::Struct,
}]
});
assert!(
!output.contains("struct Remove"),
"Struct not removed: {}",
output
);
assert!(output.contains("struct Keep"), "Other struct lost");
}
#[test]
fn test_remove_method() {
use ryo_analysis::SymbolKind;
let input = r#"
pub struct Service {}
impl Service {
pub fn keep() {}
pub fn remove_this() {}
pub fn also_keep() {}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let service_id = ctx
.registry
.iter()
.find(|(id, _path)| {
matches!(ctx.registry.kind(*id), Some(SymbolKind::Struct))
&& ctx
.registry
.resolve(*id)
.map(|p| p.name() == "Service")
.unwrap_or(false)
})
.map(|(id, _)| id)
.expect("Should find Service struct");
vec![MutationSpec::RemoveMethod {
target: MutationTargetSymbol::ById(service_id),
method_name: "remove_this".into(),
}]
});
assert!(
!output.contains("remove_this"),
"Method not removed: {}",
output
);
assert!(output.contains("keep"), "Other method lost");
assert!(output.contains("also_keep"), "Other method lost");
}
#[test]
fn test_create_mod() {
let input = "";
let output = execute_mutation(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "utils".into(),
is_pub: true,
content: "pub fn helper() {}".into(),
}],
);
assert!(
output.contains("pub mod utils"),
"Module declaration not added: {}",
output
);
}
#[test]
fn test_create_mod_declaration() {
let input = "pub fn existing() {}";
let output = execute_mutation(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "submodule".into(),
content: String::new(),
is_pub: true,
}],
);
assert!(
output.contains("pub mod submodule"),
"Module not added: {}",
output
);
}
#[test]
fn test_organize_imports_deduplication() {
let input = r#"
use std::collections::HashMap;
use std::collections::HashMap;
use std::vec::Vec;
"#;
let output = execute_mutation(
input,
vec![MutationSpec::OrganizeImports {
module_id: None,
deduplicate: true,
merge_groups: false,
}],
);
let hashmap_count = output.matches("HashMap").count();
assert!(
hashmap_count <= 1,
"Duplicate imports not removed: {}",
output
);
}
#[test]
fn test_duplicate_function() {
let input = r#"
pub fn original_function(x: i32) -> i32 {
x * 2
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "original_function");
vec![MutationSpec::DuplicateFunction {
target: MutationTargetSymbol::ById(symbol_id),
to: "cloned_function".into(),
}]
});
assert!(
output.contains("original_function"),
"Original function lost: {}",
output
);
assert!(
output.contains("cloned_function"),
"Cloned function not added: {}",
output
);
}
#[test]
fn test_duplicate_struct() {
let input = r#"
#[derive(Debug, Clone)]
pub struct Original {
pub id: u64,
pub name: String,
}
impl Original {
pub fn new(id: u64, name: String) -> Self {
Self { id, name }
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "Original");
vec![MutationSpec::DuplicateStruct {
target: MutationTargetSymbol::ById(symbol_id),
to: "Cloned".into(),
include_impls: true,
}]
});
assert!(
output.contains("struct Original"),
"Original struct lost: {}",
output
);
assert!(
output.contains("struct Cloned"),
"Cloned struct not added: {}",
output
);
if output.contains("impl Cloned") {
assert!(
output.contains("impl Cloned"),
"Impl block should be duplicated"
);
}
}
#[test]
fn test_duplicate_enum() {
let input = r#"
#[derive(Debug)]
pub enum Status {
Active,
Inactive,
Pending,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let id = lookup_symbol_id(ctx, "Status");
vec![MutationSpec::DuplicateEnum {
target: MutationTargetSymbol::ById(id),
to: "ClonedStatus".into(),
include_impls: false,
}]
});
assert!(
output.contains("enum Status"),
"Original enum lost: {}",
output
);
assert!(
output.contains("enum ClonedStatus") || output.contains("ClonedStatus"),
"Cloned enum not added: {}",
output
);
}
#[test]
fn test_duplicate_struct_without_impls() {
let input = r#"
pub struct Simple {
pub value: i32,
}
impl Simple {
pub fn get(&self) -> i32 {
self.value
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "Simple");
vec![MutationSpec::DuplicateStruct {
target: MutationTargetSymbol::ById(symbol_id),
to: "SimpleCopy".into(),
include_impls: false,
}]
});
assert!(output.contains("struct Simple"), "Original struct lost");
assert!(
output.contains("struct SimpleCopy") || output.contains("SimpleCopy"),
"Cloned struct not added: {}",
output
);
let impl_simple_copy_count = output.matches("impl SimpleCopy").count();
assert_eq!(
impl_simple_copy_count, 0,
"Impl block should not be duplicated when include_impls is false"
);
}
mod v2_snapshot_tests {
use super::*;
fn assert_v1_v2_equivalent(input: &str, specs: Vec<MutationSpec>, test_name: &str) {
let v1_output = execute_mutation(input, specs.clone());
let v2_output = execute_mutation_v2(input, specs);
let v1_normalized = normalize_source(&v1_output);
let v2_normalized = normalize_source(&v2_output);
assert!(
sources_equal(&v1_output, &v2_output),
"{}: V1 and V2 outputs differ!\n\n=== V1 (normalized) ===\n{}\n\n=== V2 (normalized) ===\n{}",
test_name,
v1_normalized,
v2_normalized
);
}
fn assert_v2_snapshot(input: &str, specs: Vec<MutationSpec>, expected: &str, test_name: &str) {
let v2_output = execute_mutation_v2(input, specs);
let v2_normalized = normalize_source(&v2_output);
let expected_normalized = normalize_source(expected);
assert!(
v2_normalized == expected_normalized,
"{}: V2 output differs from expected!\n\n=== Expected (normalized) ===\n{}\n\n=== V2 (normalized) ===\n{}",
test_name,
expected_normalized,
v2_normalized
);
}
#[test]
fn v2_add_field_basic() {
let input = r#"
pub struct User {
pub name: String,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "age".into(),
field_type: "u32".into(),
visibility: Visibility::Pub,
}],
"v2_add_field_basic",
);
}
#[test]
fn v2_add_field_private() {
let input = "pub struct Config { pub debug: bool }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "internal".into(),
field_type: "Vec<u8>".into(),
visibility: Visibility::Private,
}],
"v2_add_field_private",
);
}
#[test]
fn v2_add_field_to_empty_struct() {
let input = "pub struct Empty {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "id".into(),
field_type: "u64".into(),
visibility: Visibility::Pub,
}],
"v2_add_field_to_empty_struct",
);
}
#[test]
fn v2_add_field_with_generic_type() {
let input = "pub struct Container<T> { pub items: Vec<T> }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "capacity".into(),
field_type: "usize".into(),
visibility: Visibility::Pub,
}],
"v2_add_field_with_generic_type",
);
}
#[test]
fn v2_remove_field_basic() {
let input = r#"
pub struct User {
pub name: String,
pub age: u32,
pub email: String,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "age".into(),
}],
"v2_remove_field_basic",
);
}
#[test]
fn v2_remove_field_first() {
let input = r#"
pub struct Data {
pub first: i32,
pub second: i32,
pub third: i32,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "first".into(),
}],
"v2_remove_field_first",
);
}
#[test]
fn v2_remove_field_last() {
let input = r#"
pub struct Data {
pub first: i32,
pub second: i32,
pub third: i32,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "third".into(),
}],
"v2_remove_field_last",
);
}
#[test]
fn v2_create_mod_decl_basic() {
let input = "pub fn existing() {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "submodule".into(),
content: String::new(),
is_pub: true,
}],
"v2_create_mod_decl_basic",
);
}
#[test]
fn v2_create_mod_decl_private() {
let input = "pub struct Foo {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "internal".into(),
content: String::new(),
is_pub: false,
}],
"v2_create_mod_decl_private",
);
}
#[test]
fn v2_create_mod_decl_to_empty_file() {
let input = "";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "utils".into(),
content: String::new(),
is_pub: true,
}],
"v2_create_mod_decl_to_empty_file",
);
}
#[test]
fn v2_remove_mod_basic() {
let input = r#"
pub mod keep_me;
pub mod remove_me;
pub mod also_keep;
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "remove_me".into(),
}],
"v2_remove_mod_basic",
);
}
#[test]
fn v2_remove_mod_inline() {
let input = r#"
pub mod inline_mod {
pub fn helper() {}
}
pub fn main_fn() {}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "inline_mod".into(),
}],
"v2_remove_mod_inline",
);
}
#[test]
fn v2_add_then_remove_field() {
let input = r#"
pub struct Data {
pub value: i32,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "count".into(),
field_type: "u64".into(),
visibility: Visibility::Pub,
}],
"v2_add_then_remove_field",
);
}
#[test]
fn v2_multiple_add_fields() {
let input = "pub struct Empty {}";
assert_v1_v2_equivalent(
input,
vec![
MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "first".into(),
field_type: "i32".into(),
visibility: Visibility::Pub,
},
MutationSpec::AddField {
target: MutationTargetSymbol::ById(dummy_id(1)),
field_name: "second".into(),
field_type: "String".into(),
visibility: Visibility::Pub,
},
],
"v2_multiple_add_fields",
);
}
#[test]
fn v2_add_derive_single() {
let input = r#"
#[derive(Debug)]
pub struct Item {
pub id: u32,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".into()],
}],
"v2_add_derive_single",
);
}
#[test]
fn v2_add_derive_multiple() {
let input = "pub struct Data {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".into(), "Clone".into(), "PartialEq".into()],
}],
"v2_add_derive_multiple",
);
}
#[test]
fn v2_add_derive_to_enum() {
let input = "pub enum Color { Red, Green, Blue }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".into(), "Clone".into(), "Copy".into()],
}],
"v2_add_derive_to_enum",
);
}
#[test]
fn v2_add_derive_no_existing() {
let input = "pub struct Plain { pub value: i32 }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".into()],
}],
"v2_add_derive_no_existing",
);
}
#[test]
fn v2_remove_derive_single() {
let input = r#"
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub id: u32,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".into()],
}],
"v2_remove_derive_single",
);
}
#[test]
fn v2_remove_derive_multiple() {
let input = r#"
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Data {}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".into(), "Eq".into()],
}],
"v2_remove_derive_multiple",
);
}
#[test]
fn v2_remove_all_derives() {
let input = r#"
#[derive(Debug)]
pub struct Single {}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".into()],
}],
"v2_remove_all_derives",
);
}
#[test]
fn v2_add_variant_unit() {
let input = r#"
pub enum Status {
Active,
Inactive,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "Pending".into(),
variant_kind: VariantKind::Unit,
}],
"v2_add_variant_unit",
);
}
#[test]
fn v2_add_variant_tuple() {
let input = "pub enum Message { Text(String) }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "Binary".into(),
variant_kind: VariantKind::Tuple {
types: vec!["Vec<u8>".into()],
},
}],
"v2_add_variant_tuple",
);
}
#[test]
fn v2_add_variant_struct() {
let input = "pub enum Event { Click }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "KeyPress".into(),
variant_kind: VariantKind::Struct {
fields: vec![
("key".into(), "char".into()),
("modifiers".into(), "u8".into()),
],
},
}],
"v2_add_variant_struct",
);
}
#[test]
fn v2_add_variant_to_empty_enum() {
let input = "pub enum Empty {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "First".into(),
variant_kind: VariantKind::Unit,
}],
"v2_add_variant_to_empty_enum",
);
}
#[test]
fn v2_remove_variant_basic() {
let input = r#"
pub enum Status {
Active,
Pending,
Inactive,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "Pending".into(),
}],
"v2_remove_variant_basic",
);
}
#[test]
fn v2_remove_variant_first() {
let input = r#"
pub enum Order {
First,
Second,
Third,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "First".into(),
}],
"v2_remove_variant_first",
);
}
#[test]
fn v2_remove_variant_last() {
let input = r#"
pub enum Order {
First,
Second,
Third,
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveVariant {
target: MutationTargetSymbol::ById(dummy_id(1)),
variant_name: "Third".into(),
}],
"v2_remove_variant_last",
);
}
#[test]
fn v2_change_visibility_private_to_pub() {
let input = "struct Private { field: i32 }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(dummy_id(1)),
visibility: Visibility::Pub,
}],
"v2_change_visibility_private_to_pub",
);
}
#[test]
fn v2_change_visibility_pub_to_pub_crate() {
let input = "pub struct Public { field: i32 }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(dummy_id(1)),
visibility: Visibility::PubCrate,
}],
"v2_change_visibility_pub_to_pub_crate",
);
}
#[test]
fn v2_change_visibility_function() {
let input = "fn private_fn() {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(dummy_id(1)),
visibility: Visibility::Pub,
}],
"v2_change_visibility_function",
);
}
#[test]
fn v2_rename_struct() {
let input = "pub struct OldName { field: i32 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "OldName");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "NewName".into(),
scope: Scope::Project,
}]
});
assert!(output.contains("NewName"), "Struct not renamed: {}", output);
assert!(!output.contains("OldName"), "Old name still present");
}
#[test]
fn v2_rename_function() {
let input = "fn old_function() -> i32 { 42 }";
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "old_function");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "new_function".into(),
scope: Scope::Project,
}]
});
assert!(
output.contains("new_function"),
"Function not renamed: {}",
output
);
assert!(!output.contains("old_function"), "Old name still present");
}
#[test]
fn v2_rename_enum() {
let input = r#"
pub enum OldStatus {
Active,
Inactive,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let symbol_id = lookup_symbol_id(ctx, "OldStatus");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "Status".into(),
scope: Scope::Project,
}]
});
assert!(output.contains("Status"), "Enum not renamed: {}", output);
assert!(!output.contains("OldStatus"), "Old name still present");
}
#[test]
fn v2_add_item_struct() {
let input = "pub struct Existing { field: i32 }";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "pub struct NewStruct { name: String }".into(),
position: InsertPosition::Bottom,
}],
"v2_add_item_struct",
);
}
#[test]
fn v2_add_item_function() {
let input = "fn existing() {}";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "pub fn new_function(x: i32) -> i32 { x * 2 }".into(),
position: InsertPosition::Bottom,
}],
"v2_add_item_function",
);
}
#[test]
fn v2_add_item_enum() {
let input = "struct Foo;";
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
content: "pub enum Status { Active, Inactive }".into(),
position: InsertPosition::Bottom,
}],
"v2_add_item_enum",
);
}
#[test]
fn v2_remove_item_struct() {
let input = r#"
pub struct ToRemove { field: i32 }
pub struct ToKeep { name: String }
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(dummy_id(1)),
item_kind: ItemKind::Struct,
}],
"v2_remove_item_struct",
);
}
#[test]
fn v2_remove_item_function() {
let input = r#"
fn to_remove() {}
fn to_keep() {}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(dummy_id(1)),
item_kind: ItemKind::Function,
}],
"v2_remove_item_function",
);
}
#[test]
fn v2_remove_item_enum() {
let input = r#"
pub enum ToRemove { A, B }
pub enum ToKeep { X, Y }
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(dummy_id(1)),
item_kind: ItemKind::Enum,
}],
"v2_remove_item_enum",
);
}
#[test]
fn v2_add_method_basic() {
let input = r#"
pub struct Counter {
value: i32,
}
impl Counter {
pub fn new() -> Self {
Self { value: 0 }
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let counter_id = lookup_symbol_id(ctx, "Counter");
vec![MutationSpec::AddMethod {
target: MutationTargetSymbol::ById(counter_id),
method_name: "increment".into(),
params: vec![],
return_type: None,
body: "self.value += 1".into(),
is_pub: true,
self_param: Some(SelfParam::Mut),
}]
});
assert!(
output.contains("fn increment"),
"Should add increment method"
);
assert!(
output.contains("&mut self"),
"Should have &mut self parameter"
);
assert!(
output.contains("self.value += 1"),
"Should have correct body"
);
}
#[test]
fn v2_add_method_with_params() {
let input = r#"
pub struct Calculator;
impl Calculator {
pub fn new() -> Self { Self }
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let calculator_id = lookup_symbol_id(ctx, "Calculator");
vec![MutationSpec::AddMethod {
target: MutationTargetSymbol::ById(calculator_id),
method_name: "add".into(),
params: vec![("a".into(), "i32".into()), ("b".into(), "i32".into())],
return_type: Some("i32".into()),
body: "a + b".into(),
is_pub: true,
self_param: Some(SelfParam::Ref),
}]
});
assert!(output.contains("fn add"), "Should add method");
assert!(output.contains("&self"), "Should have &self");
assert!(output.contains("a: i32"), "Should have param a");
assert!(output.contains("b: i32"), "Should have param b");
assert!(output.contains("-> i32"), "Should have return type");
}
#[test]
fn v2_add_method_static() {
let input = r#"
pub struct Factory;
impl Factory {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let factory_id = lookup_symbol_id(ctx, "Factory");
vec![MutationSpec::AddMethod {
target: MutationTargetSymbol::ById(factory_id),
method_name: "create".into(),
params: vec![("name".into(), "String".into())],
return_type: Some("Self".into()),
body: "Self".into(),
is_pub: true,
self_param: None,
}]
});
assert!(output.contains("fn create"), "Should add static method");
assert!(output.contains("name: String"), "Should have name param");
assert!(output.contains("-> Self"), "Should return Self");
assert!(
!output.contains("&self") && !output.contains("&mut self"),
"Static method should not have self param"
);
}
#[test]
fn v2_remove_method_basic() {
use ryo_analysis::SymbolKind;
let input = r#"
pub struct Counter {
value: i32,
}
impl Counter {
pub fn new() -> Self {
Self { value: 0 }
}
pub fn to_remove(&self) -> i32 {
self.value
}
pub fn get(&self) -> i32 {
self.value
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let counter_id = ctx
.registry
.iter()
.find(|(id, _path)| {
matches!(ctx.registry.kind(*id), Some(SymbolKind::Struct))
&& ctx
.registry
.resolve(*id)
.map(|p| p.name() == "Counter")
.unwrap_or(false)
})
.map(|(id, _)| id)
.expect("Should find Counter struct");
vec![MutationSpec::RemoveMethod {
target: MutationTargetSymbol::ById(counter_id),
method_name: "to_remove".into(),
}]
});
assert!(
!output.contains("fn to_remove"),
"Should remove to_remove method"
);
assert!(output.contains("fn new"), "Should keep new method");
assert!(output.contains("fn get"), "Should keep get method");
}
#[test]
fn v2_remove_method_with_params() {
use ryo_analysis::SymbolKind;
let input = r#"
pub struct Calculator;
impl Calculator {
pub fn add(&self, a: i32, b: i32) -> i32 { a + b }
pub fn sub(&self, a: i32, b: i32) -> i32 { a - b }
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let calculator_id = ctx
.registry
.iter()
.find(|(id, _path)| {
matches!(ctx.registry.kind(*id), Some(SymbolKind::Struct))
&& ctx
.registry
.resolve(*id)
.map(|p| p.name() == "Calculator")
.unwrap_or(false)
})
.map(|(id, _)| id)
.expect("Should find Calculator struct");
vec![MutationSpec::RemoveMethod {
target: MutationTargetSymbol::ById(calculator_id),
method_name: "sub".into(),
}]
});
assert!(output.contains("fn add"), "Should keep add method");
assert!(!output.contains("fn sub"), "Should remove sub method");
}
#[test]
fn v2_create_mod_basic() {
let input = "pub fn existing() {}";
let output = execute_mutation_v2(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "utils".into(),
content: "pub fn helper() {}".into(),
is_pub: true,
}],
);
assert!(
output.contains("pub mod utils"),
"Should add module declaration"
);
assert!(
output.contains("fn existing"),
"Should keep existing function"
);
}
#[test]
fn v2_create_mod_private() {
let input = "";
let output = execute_mutation_v2(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "internal".into(),
content: "fn private_fn() {}".into(),
is_pub: false,
}],
);
assert!(
output.contains("mod internal"),
"Should add private module declaration"
);
assert!(!output.contains("pub mod internal"), "Should not be public");
}
#[test]
fn v2_create_mod_empty_content() {
let input = "pub struct Foo {}";
let output = execute_mutation_v2(
input,
vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(crate_root())),
mod_name: "empty".into(),
content: "".into(),
is_pub: true,
}],
);
assert!(
output.contains("pub mod empty"),
"Should add empty module declaration"
);
}
#[test]
fn v2_add_match_arm_basic() {
let input = r#"
enum Status {
Active,
Inactive,
}
fn process(status: Status) -> &'static str {
match status {
Status::Active => "active",
Status::Inactive => "inactive",
}
}
"#;
let expected = r#"
enum Status {
Active,
Inactive,
}
fn process(status: Status) -> &'static str {
match status {
Status::Active => "active",
Status::Inactive => "inactive",
Status::Pending => "pending",
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::process").unwrap(),
)),
enum_name: "Status".into(),
pattern: "Status::Pending".into(),
body: "\"pending\"".into(),
}],
expected,
"v2_add_match_arm_basic",
);
}
#[test]
fn v2_add_match_arm_with_wildcard() {
let input = r#"
enum Color { Red, Green, Blue }
fn name(c: Color) -> &'static str {
match c {
Color::Red => "red",
_ => "other",
}
}
"#;
let expected = r#"
enum Color {
Red,
Green,
Blue,
}
fn name(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
_ => "other",
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::name").unwrap(),
)),
enum_name: "Color".into(),
pattern: "Color::Green".into(),
body: "\"green\"".into(),
}],
expected,
"v2_add_match_arm_with_wildcard",
);
}
#[test]
fn v2_add_match_arm_tuple_variant() {
let input = r#"
enum PathSegment {
Key(String),
Index(usize),
}
fn process(seg: PathSegment) -> String {
match seg {
PathSegment::Key(k) => k,
PathSegment::Index(i) => i.to_string(),
}
}
"#;
let expected = r#"
enum PathSegment {
Key(String),
Index(usize),
}
fn process(seg: PathSegment) -> String {
match seg {
PathSegment::Key(k) => k,
PathSegment::Index(i) => i.to_string(),
PathSegment::Slice(_, _) => todo!(),
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::process").unwrap(),
)),
enum_name: "PathSegment".into(),
pattern: "PathSegment::Slice(_, _)".into(),
body: "todo!()".into(),
}],
expected,
"v2_add_match_arm_tuple_variant",
);
}
#[test]
fn v2_add_match_arm_struct_variant() {
let input = r#"
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
}
fn handle(e: Event) -> String {
match e {
Event::Click { x, y } => format!("click at ({}, {})", x, y),
Event::KeyPress(c) => format!("key: {}", c),
}
}
"#;
let expected = r#"
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
}
fn handle(e: Event) -> String {
match e {
Event::Click { x: x, y: y } => format!("click at ({}, {})", x, y),
Event::KeyPress(c) => format!("key: {}", c),
Event::Scroll { dx: _, dy: _ } => todo!(),
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::handle").unwrap(),
)),
enum_name: "Event".into(),
pattern: "Event::Scroll { dx: _, dy: _ }".into(),
body: "todo!()".into(),
}],
expected,
"v2_add_match_arm_struct_variant",
);
}
#[test]
fn v2_add_match_arm_rest_pattern() {
let input = r#"
enum Data {
Single(i32),
Pair(i32, i32),
}
fn sum(d: Data) -> i32 {
match d {
Data::Single(x) => x,
Data::Pair(x, y) => x + y,
}
}
"#;
let expected = r#"
enum Data {
Single(i32),
Pair(i32, i32),
}
fn sum(d: Data) -> i32 {
match d {
Data::Single(x) => x,
Data::Pair(x, y) => x + y,
Data::Triple(..) => todo!(),
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::sum").unwrap(),
)),
enum_name: "Data".into(),
pattern: "Data::Triple(..)".into(),
body: "todo!()".into(),
}],
expected,
"v2_add_match_arm_rest_pattern",
);
}
#[test]
fn v2_remove_match_arm_basic() {
let input = r#"
enum Level { Low, Medium, High }
fn priority(l: Level) -> u8 {
match l {
Level::Low => 1,
Level::Medium => 5,
Level::High => 10,
}
}
"#;
let expected = r#"
enum Level {
Low,
Medium,
High,
}
fn priority(l: Level) -> u8 {
match l {
Level::Low => 1,
Level::High => 10,
}
}
"#;
assert_v2_snapshot(
input,
vec![MutationSpec::RemoveMatchArm {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::priority").unwrap(),
)),
enum_name: "Level".into(),
pattern: "Level::Medium".into(),
}],
expected,
"v2_remove_match_arm_basic",
);
}
#[test]
fn v2_add_struct_literal_field_basic() {
let input = r#"
struct Config {
name: String,
timeout: Option<u64>,
}
fn default_config() -> Config {
Config {
name: "default".to_string(),
}
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddStructLiteralField {
target: MutationTargetSymbol::ByKindAndName(ItemKind::Struct, "Config".into()),
field_name: "timeout".into(),
value: "None".into(),
}],
"v2_add_struct_literal_field_basic",
);
}
#[test]
fn v2_add_struct_literal_field_with_self() {
let input = r#"
struct Point {
x: i32,
y: i32,
z: i32,
}
impl Point {
fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::AddStructLiteralField {
target: MutationTargetSymbol::ByKindAndName(ItemKind::Struct, "Point".into()),
field_name: "z".into(),
value: "0".into(),
}],
"v2_add_struct_literal_field_with_self",
);
}
#[test]
fn v2_remove_struct_literal_field_basic() {
let input = r#"
struct Options {
debug: bool,
verbose: bool,
}
fn default_options() -> Options {
Options {
debug: false,
verbose: true,
}
}
"#;
assert_v1_v2_equivalent(
input,
vec![MutationSpec::RemoveStructLiteralField {
target: MutationTargetSymbol::ByKindAndName(ItemKind::Struct, "Options".into()),
field_name: "verbose".into(),
}],
"v2_remove_struct_literal_field_basic",
);
}
#[test]
fn v2_extract_trait_no_pub_in_impl() {
use ryo_analysis::SymbolKind;
use ryo_source::pure::PureItem;
let input = r#"
pub struct Storage {
path: String,
}
impl Storage {
pub fn save(&self, data: &str) -> bool {
let _ = data;
true
}
pub fn load(&self) -> String {
String::new()
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let impl_id = ctx
.registry
.iter()
.find(|(id, _path)| {
if !matches!(ctx.registry.kind(*id), Some(SymbolKind::Impl)) {
return false;
}
if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
imp.trait_.is_none() && imp.self_ty == "Storage"
} else {
false
}
})
.map(|(id, _)| id)
.expect("Should find impl Storage");
vec![MutationSpec::ExtractTrait {
target: MutationTargetSymbol::ById(impl_id),
trait_name: "Storable".into(),
methods: None, }]
});
assert!(
output.contains("trait Storable"),
"Should contain trait Storable: {}",
output
);
assert!(
output.contains("impl Storable for Storage"),
"Should contain impl Storable for Storage: {}",
output
);
}
#[test]
fn v2_extract_trait_partial_methods() {
use ryo_analysis::SymbolKind;
use ryo_source::pure::PureItem;
let input = r#"
pub struct Cache {
data: Vec<u8>,
}
impl Cache {
pub fn get(&self, key: &str) -> Option<&[u8]> {
let _ = key;
None
}
pub fn set(&mut self, key: &str, value: &[u8]) {
let _ = (key, value);
}
pub fn clear(&mut self) {
self.data.clear();
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let impl_id = ctx
.registry
.iter()
.find(|(id, _path)| {
if !matches!(ctx.registry.kind(*id), Some(SymbolKind::Impl)) {
return false;
}
if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
imp.trait_.is_none() && imp.self_ty == "Cache"
} else {
false
}
})
.map(|(id, _)| id)
.expect("Should find impl Cache");
vec![MutationSpec::ExtractTrait {
target: MutationTargetSymbol::ById(impl_id),
trait_name: "CacheOps".into(),
methods: Some(vec!["get".into(), "set".into()]),
}]
});
assert!(
output.contains("trait CacheOps"),
"Should contain trait CacheOps: {}",
output
);
assert!(
output.contains("impl CacheOps for Cache"),
"Should contain impl CacheOps for Cache: {}",
output
);
assert!(
output.contains("fn clear"),
"Should still have clear method: {}",
output
);
}
}
mod v2_cross_file_tests {
use super::*;
#[test]
fn v2_move_item_struct_to_submodule() {
let lib_rs = r#"
pub struct Task {
pub id: u32,
pub name: String,
}
pub mod core;
"#;
let core_rs = "//! Core module\n";
let files = vec![("src/lib.rs", lib_rs), ("src/core.rs", core_rs)];
let results = execute_mutation_v2_multi(
&files,
vec![MutationSpec::MoveItem {
source: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::core").unwrap(),
)),
item_name: "Task".into(),
item_kind: ItemKind::Struct,
add_use: false,
}],
);
let lib_output = results.get(&PathBuf::from("src/lib.rs")).unwrap();
let core_output = results.get(&PathBuf::from("src/core.rs")).unwrap();
assert!(
!lib_output.contains("struct Task"),
"Task should be removed from lib.rs: {}",
lib_output
);
assert!(
lib_output.contains("pub mod core"),
"Module declaration should remain"
);
assert!(
core_output.contains("struct Task"),
"Task should be in core.rs: {}",
core_output
);
assert!(
core_output.contains("pub id: u32"),
"Task fields should be preserved"
);
}
#[test]
fn v2_move_item_with_add_use() {
let lib_rs = r#"
pub struct Config {
pub debug: bool,
}
pub mod settings;
"#;
let settings_rs = "//! Settings module\n";
let files = vec![("src/lib.rs", lib_rs), ("src/settings.rs", settings_rs)];
let results = execute_mutation_v2_multi(
&files,
vec![MutationSpec::MoveItem {
source: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::settings").unwrap(),
)),
item_name: "Config".into(),
item_kind: ItemKind::Struct,
add_use: true,
}],
);
let lib_output = results.get(&PathBuf::from("src/lib.rs")).unwrap();
let settings_output = results.get(&PathBuf::from("src/settings.rs")).unwrap();
assert!(
!lib_output.contains("struct Config"),
"Config should be removed from lib.rs"
);
assert!(
lib_output.contains("use test_crate::settings::Config")
|| lib_output.contains("use crate::settings::Config")
|| lib_output.contains("use settings::Config"),
"Use statement should be added to lib.rs: {}",
lib_output
);
assert!(
settings_output.contains("struct Config"),
"Config should be in settings.rs: {}",
settings_output
);
}
#[test]
fn v2_move_item_function() {
let lib_rs = r#"
pub fn helper_function(x: i32) -> i32 {
x * 2
}
pub mod utils;
"#;
let utils_rs = "//! Utils module\n";
let files = vec![("src/lib.rs", lib_rs), ("src/utils.rs", utils_rs)];
let results = execute_mutation_v2_multi(
&files,
vec![MutationSpec::MoveItem {
source: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::utils").unwrap(),
)),
item_name: "helper_function".into(),
item_kind: ItemKind::Function,
add_use: false,
}],
);
let lib_output = results.get(&PathBuf::from("src/lib.rs")).unwrap();
let utils_output = results.get(&PathBuf::from("src/utils.rs")).unwrap();
assert!(
!lib_output.contains("fn helper_function"),
"Function should be removed from lib.rs"
);
assert!(
utils_output.contains("fn helper_function"),
"Function should be in utils.rs: {}",
utils_output
);
assert!(
utils_output.contains("x * 2"),
"Function body should be preserved"
);
}
#[test]
fn v2_move_item_enum() {
let lib_rs = r#"
#[derive(Debug, Clone)]
pub enum Status {
Active,
Inactive,
Pending,
}
pub mod types;
"#;
let types_rs = "//! Types module\n";
let files = vec![("src/lib.rs", lib_rs), ("src/types.rs", types_rs)];
let results = execute_mutation_v2_multi(
&files,
vec![MutationSpec::MoveItem {
source: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::types").unwrap(),
)),
item_name: "Status".into(),
item_kind: ItemKind::Enum,
add_use: false,
}],
);
let lib_output = results.get(&PathBuf::from("src/lib.rs")).unwrap();
let types_output = results.get(&PathBuf::from("src/types.rs")).unwrap();
assert!(
!lib_output.contains("enum Status"),
"Enum should be removed from lib.rs"
);
assert!(
types_output.contains("enum Status"),
"Enum should be in types.rs: {}",
types_output
);
assert!(
types_output.contains("Active"),
"Enum variants should be preserved"
);
assert!(
types_output.contains("derive"),
"Derives should be preserved"
);
}
}
mod v2_inline_trait_tests {
use super::*;
#[test]
fn v2_inline_trait_basic() {
let input = r#"
pub trait Incrementable {
fn increment(&mut self);
}
pub struct Counter {
value: i32,
}
impl Incrementable for Counter {
fn increment(&mut self) {
self.value += 1;
}
}
"#;
let expected = r#"
pub struct Counter {
value: i32,
}
impl Counter {
fn increment(&mut self) {
self.value += 1;
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let trait_id = lookup_symbol_id(ctx, "Incrementable");
vec![MutationSpec::InlineTrait {
target: MutationTargetSymbol::ById(trait_id),
struct_name: "Counter".into(),
remove_trait: true,
}]
});
let _output_normalized = normalize_source(&output);
let _expected_normalized = normalize_source(expected);
assert!(
!output.contains("trait Incrementable"),
"Trait should be removed: {}",
output
);
assert!(
output.contains("impl Counter"),
"Should have inherent impl: {}",
output
);
assert!(
output.contains("fn increment"),
"Method should be present: {}",
output
);
assert!(
output.contains("self.value += 1"),
"Method body should be preserved"
);
assert!(
!output.contains("impl Incrementable for"),
"Trait impl should be removed: {}",
output
);
}
#[test]
fn v2_inline_trait_keep_trait() {
let input = r#"
pub trait Displayable {
fn display(&self) -> String;
}
pub struct Item {
name: String,
}
impl Displayable for Item {
fn display(&self) -> String {
self.name.clone()
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let trait_id = lookup_symbol_id(ctx, "Displayable");
vec![MutationSpec::InlineTrait {
target: MutationTargetSymbol::ById(trait_id),
struct_name: "Item".into(),
remove_trait: false, }]
});
assert!(
output.contains("trait Displayable"),
"Trait should be kept: {}",
output
);
assert!(
output.contains("impl Item"),
"Should have inherent impl: {}",
output
);
assert!(output.contains("fn display"), "Method should be present");
}
#[test]
fn v2_inline_trait_multiple_methods() {
let input = r#"
pub trait Calculator {
fn add(&self, a: i32, b: i32) -> i32;
fn subtract(&self, a: i32, b: i32) -> i32;
}
pub struct BasicCalc;
impl Calculator for BasicCalc {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
fn subtract(&self, a: i32, b: i32) -> i32 {
a - b
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let trait_id = lookup_symbol_id(ctx, "Calculator");
vec![MutationSpec::InlineTrait {
target: MutationTargetSymbol::ById(trait_id),
struct_name: "BasicCalc".into(),
remove_trait: true,
}]
});
assert!(output.contains("fn add"), "add method should be present");
assert!(
output.contains("fn subtract"),
"subtract method should be present"
);
assert!(output.contains("a + b"), "add body should be preserved");
assert!(
output.contains("a - b"),
"subtract body should be preserved"
);
}
}
mod v2_statement_tests {
use super::*;
#[test]
fn v2_insert_statement_at_beginning() {
let input = r#"
fn process(x: i32) -> i32 {
let result = x * 2;
result
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "process");
vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: "println!(\"entering process\");".into(),
position: StmtInsertPosition::Start,
reference_pattern: None,
symbol_path: None,
}]
});
assert!(
output.contains("println!"),
"Statement should be inserted: {}",
output
);
let println_pos = output.find("println!").unwrap();
let let_pos = output.find("let result").unwrap();
assert!(
println_pos < let_pos,
"println should be before let statement"
);
}
#[test]
fn v2_insert_statement_at_end() {
let input = r#"
fn compute(x: i32) -> i32 {
let value = x + 1;
value
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "compute");
vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: "println!(\"done\");".into(),
position: StmtInsertPosition::End,
reference_pattern: None,
symbol_path: None,
}]
});
assert!(
output.contains("println!(\"done\")"),
"Statement should be inserted: {}",
output
);
}
#[test]
fn v2_insert_statement_after_pattern() {
let input = r#"
fn initialize() {
let config = load_config();
let data = process_data();
save_result();
}
fn load_config() -> i32 { 0 }
fn process_data() -> i32 { 0 }
fn save_result() {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "initialize");
vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: "validate_config();".into(),
position: StmtInsertPosition::AfterPattern,
reference_pattern: Some("let config = load_config()".into()),
symbol_path: None,
}]
});
assert!(
output.contains("validate_config"),
"Statement should be inserted: {}",
output
);
}
#[test]
fn v2_remove_statement_single() {
let input = r#"
fn example() {
println!("debug info");
let x = 42;
println!("more debug");
x
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "example");
vec![MutationSpec::RemoveStatement {
module_id: None,
fn_id: Some(fn_id),
pattern: "println!(..)".into(), remove_all: false,
symbol_path: None,
}]
});
assert!(
output.contains("println!"),
"Second println should remain: {}",
output
);
assert!(output.contains("let x = 42"), "let statement should remain");
let println_count = output.matches("println!").count();
assert_eq!(
println_count, 1,
"Should have exactly 1 println remaining: {}",
output
);
}
#[test]
fn v2_remove_statement_all() {
let input = r#"
fn debug_function() {
println!("start");
let a = 1;
println!("middle");
let b = 2;
println!("end");
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "debug_function");
vec![MutationSpec::RemoveStatement {
module_id: None,
fn_id: Some(fn_id),
pattern: "println!(..)".into(),
remove_all: true,
symbol_path: None,
}]
});
assert!(
!output.contains("println!"),
"All println should be removed: {}",
output
);
assert!(output.contains("let a = 1"), "let a should remain");
assert!(output.contains("let b = 2"), "let b should remain");
}
#[test]
fn v2_replace_statement_basic() {
let input = r#"
fn update() {
let old_value = 10;
process(old_value);
}
fn process(_: i32) {}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "update");
vec![MutationSpec::ReplaceStatement {
module_id: None,
fn_id: Some(fn_id),
old_stmt: "let old_value = 10".into(),
new_stmt: "let new_value = 20".into(),
symbol_path: None,
}]
});
assert!(
!output.contains("old_value = 10"),
"Old statement should be replaced: {}",
output
);
assert!(
output.contains("new_value = 20"),
"New statement should be present: {}",
output
);
}
#[test]
fn v2_replace_expr_basic() {
let input = r#"
fn calculate() -> i32 {
let x = old_computation();
x
}
fn old_computation() -> i32 { 0 }
fn new_computation() -> i32 { 1 }
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "calculate");
vec![MutationSpec::ReplaceExpr {
module_id: None,
fn_id: Some(fn_id),
old_expr: "old_computation()".into(),
new_expr: "new_computation()".into(),
replace_all: true,
symbol_path: None,
}]
});
assert!(
!output.contains("old_computation()") || output.contains("fn old_computation"),
"old_computation call should be replaced: {}",
output
);
assert!(
output.contains("new_computation()"),
"new_computation call should be present: {}",
output
);
}
#[test]
fn v2_replace_expr_multiple() {
let input = r#"
fn multi_replace() -> i32 {
let a = MAGIC_VALUE;
let b = MAGIC_VALUE;
a + b
}
const MAGIC_VALUE: i32 = 42;
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "multi_replace");
vec![MutationSpec::ReplaceExpr {
module_id: None,
fn_id: Some(fn_id),
old_expr: "MAGIC_VALUE".into(),
new_expr: "100".into(),
replace_all: true,
symbol_path: None,
}]
});
assert!(
output.contains("const MAGIC_VALUE"),
"Const should remain: {}",
output
);
let fn_start = output.find("fn multi_replace").unwrap();
let fn_body = &output[fn_start..];
assert!(
fn_body.contains("100"),
"Replacements should be present: {}",
output
);
}
#[test]
fn v2_preserve_type_annotation_on_let() {
let input = r#"
fn process() {
let args: Vec<String> = vec![];
let count: usize = 0;
let map: std::collections::HashMap<String, i32> = Default::default();
println!("{}", count);
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "process");
vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: "let inserted = true;".into(),
position: StmtInsertPosition::Start,
reference_pattern: None,
symbol_path: None,
}]
});
assert!(
output.contains(": Vec<String>"),
"Type annotation Vec<String> should be preserved: {}",
output
);
assert!(
output.contains(": usize"),
"Type annotation usize should be preserved: {}",
output
);
assert!(
output.contains("HashMap<String, i32>"),
"Type annotation HashMap should be preserved: {}",
output
);
}
#[test]
fn v2_preserve_mut_type_annotation() {
let input = r#"
fn mutate() {
let mut buffer: Vec<u8> = Vec::new();
let mut count: usize = 0;
buffer.push(1);
count += 1;
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "mutate");
vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: "println!(\"start\");".into(),
position: StmtInsertPosition::Start,
reference_pattern: None,
symbol_path: None,
}]
});
assert!(
output.contains("mut buffer: Vec<u8>"),
"mut type annotation should be preserved: {}",
output
);
assert!(
output.contains("mut count: usize"),
"mut type annotation should be preserved: {}",
output
);
}
}
mod v2_duplicate_mod_tree_tests {
use super::*;
#[test]
fn v2_duplicate_mod_tree_basic() {
let lib_rs = r#"
pub mod original {
pub struct Data {
pub value: i32,
}
pub fn process(d: &Data) -> i32 {
d.value * 2
}
}
"#;
let output = execute_mutation_with_symbol_id(lib_rs, |ctx| {
let mod_id = lookup_symbol_id(ctx, "original");
vec![MutationSpec::DuplicateModTree {
target: MutationTargetSymbol::ById(mod_id),
to: "cloned".into(),
}]
});
assert!(
output.contains("mod original"),
"Original module should remain: {}",
output
);
assert!(
output.contains("mod cloned"),
"Cloned module should be created: {}",
output
);
assert!(output.contains("struct Data"), "Data struct should exist");
assert!(
output.contains("fn process"),
"process function should exist"
);
}
#[test]
fn v2_duplicate_mod_tree_with_nested() {
let lib_rs = r#"
pub mod parent {
pub mod child {
pub struct Inner {
pub id: u32,
}
}
pub struct Outer {
pub name: String,
}
}
"#;
let output = execute_mutation_with_symbol_id(lib_rs, |ctx| {
let mod_id = lookup_symbol_id(ctx, "parent");
vec![MutationSpec::DuplicateModTree {
target: MutationTargetSymbol::ById(mod_id),
to: "parent_copy".into(),
}]
});
assert!(
output.contains("mod parent"),
"Original module should remain"
);
assert!(
output.contains("mod parent_copy"),
"Copied module should be created: {}",
output
);
}
}
mod symbol_id_tests {
use super::*;
#[test]
fn test_add_field_with_symbol_id() {
let input = r#"
pub struct User {
pub name: String,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "User");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(struct_id),
field_name: "age".into(),
field_type: "u32".into(),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub age: u32"),
"Field not added with SymbolId: {}",
output
);
assert!(output.contains("pub name: String"), "Original field lost");
}
#[test]
fn test_add_field_with_symbol_id_multiple_structs() {
let input = r#"
pub struct User {
pub name: String,
}
pub struct Config {
pub debug: bool,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let config_id = lookup_symbol_id(ctx, "Config");
vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(config_id),
field_name: "timeout".into(),
field_type: "u64".into(),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub timeout: u64"),
"Field not added to Config: {}",
output
);
assert!(!output.contains("timeout") || output.contains("Config"));
}
#[test]
fn test_add_derive_with_symbol_id() {
let input = r#"
#[derive(Debug)]
pub struct Item {
pub id: u32,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "Item");
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(struct_id),
derives: vec!["Clone".into(), "PartialEq".into()],
}]
});
assert!(
output.contains("Clone"),
"Clone derive not added: {}",
output
);
assert!(
output.contains("PartialEq"),
"PartialEq derive not added: {}",
output
);
assert!(output.contains("Debug"), "Original derive lost");
}
#[test]
fn test_remove_field_with_symbol_id() {
let input = r#"
pub struct User {
pub name: String,
pub age: u32,
pub email: String,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "User");
vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(struct_id),
field_name: "age".into(),
}]
});
assert!(
!output.contains("age"),
"Field 'age' should be removed: {}",
output
);
assert!(
output.contains("pub name: String"),
"name field should remain"
);
assert!(
output.contains("pub email: String"),
"email field should remain"
);
}
#[test]
fn test_add_variant_with_symbol_id() {
let input = r#"
pub enum Status {
Active,
Inactive,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let enum_id = lookup_symbol_id(ctx, "Status");
vec![MutationSpec::AddVariant {
target: MutationTargetSymbol::ById(enum_id),
variant_name: "Pending".into(),
variant_kind: VariantKind::Unit,
}]
});
assert!(
output.contains("Pending"),
"Variant not added with SymbolId: {}",
output
);
assert!(output.contains("Active"), "Original variant lost");
}
#[test]
fn test_remove_variant_with_symbol_id() {
let input = r#"
pub enum Color {
Red,
Green,
Blue,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let enum_id = lookup_symbol_id(ctx, "Color");
vec![MutationSpec::RemoveVariant {
target: MutationTargetSymbol::ById(enum_id),
variant_name: "Green".into(),
}]
});
assert!(
!output.contains("Green"),
"Green variant should be removed: {}",
output
);
assert!(output.contains("Red"), "Red should remain");
assert!(output.contains("Blue"), "Blue should remain");
}
#[test]
fn test_change_visibility_with_symbol_id() {
let input = r#"
struct Internal {
data: Vec<u8>,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "Internal");
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(struct_id),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub struct Internal"),
"Visibility not changed: {}",
output
);
}
#[test]
#[ignore = "Fields don't have individual SymbolIds - changing field visibility requires struct SymbolId + field name, but current V2 path doesn't support this"]
fn test_change_field_visibility_with_symbol_id() {
let input = r#"
pub struct Config {
debug: bool,
timeout: u64,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "Config");
vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(struct_id),
visibility: Visibility::Pub,
}]
});
assert!(
output.contains("pub debug: bool"),
"Field visibility not changed: {}",
output
);
}
#[test]
fn test_rename_with_symbol_id() {
let input = r#"
pub struct OldName {
pub value: i32,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "OldName");
vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(struct_id),
to: "NewName".into(),
scope: Scope::Project,
}]
});
assert!(
output.contains("pub struct NewName"),
"Struct not renamed: {}",
output
);
assert!(
!output.contains("OldName"),
"Old name should not exist: {}",
output
);
}
#[test]
fn test_remove_derive_with_symbol_id() {
let input = r#"
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
pub id: u32,
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let struct_id = lookup_symbol_id(ctx, "Item");
vec![MutationSpec::RemoveDerive {
target: MutationTargetSymbol::ById(struct_id),
derives: vec!["Clone".into()],
}]
});
assert!(
!output.contains("Clone"),
"Clone should be removed: {}",
output
);
assert!(output.contains("Debug"), "Debug should remain");
assert!(output.contains("PartialEq"), "PartialEq should remain");
}
#[test]
fn test_duplicate_mod_tree_with_symbol_id() {
let input = r#"
mod utils {
pub fn helper() {}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let mod_id = lookup_symbol_id(ctx, "utils");
vec![MutationSpec::DuplicateModTree {
target: MutationTargetSymbol::ById(mod_id),
to: "utils_copy".into(),
}]
});
assert!(
output.contains("mod utils"),
"Original module should remain"
);
assert!(
output.contains("mod utils_copy"),
"Copied module should be created: {}",
output
);
}
#[test]
fn test_add_match_arm_with_symbol_id() {
let input = r#"
enum Status {
Active,
Inactive,
}
fn process(s: Status) -> &'static str {
match s {
Status::Active => "active",
Status::Inactive => "inactive",
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "process");
vec![MutationSpec::AddMatchArm {
target: MutationTargetSymbol::ById(fn_id),
enum_name: "Status".into(),
pattern: "Status::Pending".into(),
body: "\"pending\"".into(),
}]
});
assert!(
output.contains("Status::Pending"),
"Match arm not added with SymbolId: {}",
output
);
assert!(
output.contains("Status::Active"),
"Original arm should remain"
);
}
#[test]
fn test_remove_match_arm_with_symbol_id() {
let input = r#"
enum Level {
Low,
Medium,
High,
}
fn priority(l: Level) -> u8 {
match l {
Level::Low => 1,
Level::Medium => 5,
Level::High => 10,
}
}
"#;
let output = execute_mutation_with_symbol_id(input, |ctx| {
let fn_id = lookup_symbol_id(ctx, "priority");
vec![MutationSpec::RemoveMatchArm {
target: MutationTargetSymbol::ById(fn_id),
enum_name: "Level".into(),
pattern: "Level::Medium".into(),
}]
});
assert!(
!output.contains("Level::Medium"),
"Match arm should be removed: {}",
output
);
assert!(output.contains("Level::Low"), "Low should remain");
assert!(output.contains("Level::High"), "High should remain");
}
}
mod path_resolution_tests {
use super::*;
#[test]
#[ignore = "Multi-file tests using execute_mutation_v2_multi with dummy_id don't work - V2 path requires valid SymbolIds from registry lookup"]
fn test_add_derive_with_full_path() {
let lib_rs = r#"
mod user;
mod query;
"#;
let user_rs = r#"
pub enum UserStatus {
Active,
Inactive,
}
"#;
let query_user_rs = r#"
pub enum UserStatus {
Active,
Inactive,
}
"#;
let output = execute_mutation_v2_multi(
&[
("src/lib.rs", lib_rs),
("src/user.rs", user_rs),
("src/query/user.rs", query_user_rs),
],
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Hash".into()],
}],
);
let user_output = output
.get(&PathBuf::from("src/user.rs"))
.expect("user.rs should exist");
let query_output = output
.get(&PathBuf::from("src/query/user.rs"))
.expect("query/user.rs should exist");
assert!(
user_output.contains("Hash"),
"user::UserStatus should have Hash derive: {}",
user_output
);
assert!(
!query_output.contains("Hash"),
"query::user::UserStatus should NOT have Hash derive: {}",
query_output
);
}
#[test]
fn test_add_derive_with_short_name_returns_ambiguous() {
let lib_rs = r#"
mod user;
mod query;
"#;
let user_rs = r#"
pub enum UserStatus {
Active,
Inactive,
}
"#;
let query_user_rs = r#"
pub enum UserStatus {
Active,
Inactive,
}
"#;
let output = execute_mutation_v2_multi(
&[
("src/lib.rs", lib_rs),
("src/user.rs", user_rs),
("src/query/user.rs", query_user_rs),
],
vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Hash".into()],
}],
);
let user_output = output
.get(&PathBuf::from("src/user.rs"))
.expect("user.rs should exist");
let query_output = output
.get(&PathBuf::from("src/query/user.rs"))
.expect("query/user.rs should exist");
assert!(
!user_output.contains("Hash") && !query_output.contains("Hash"),
"Neither UserStatus should have Hash derive when target is ambiguous"
);
}
}