use std::path::Path;
use merge::{ConflictMarkers, MergeOutcome};
use super::{MergeStrategy, semantic_three_way_merge, three_way_merge};
const MARKERS: ConflictMarkers<'static> = ConflictMarkers {
ours: "OURS",
theirs: "THEIRS",
};
fn merge_rust(base: &str, ours: &str, theirs: &str) -> MergeOutcome {
semantic_three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new("a.rs"),
MARKERS,
)
}
fn assert_clean(outcome: MergeOutcome) -> String {
match outcome {
MergeOutcome::Clean(bytes) => String::from_utf8(bytes).expect("UTF-8"),
other => panic!("expected Clean, got {other:?}"),
}
}
fn assert_conflicts(outcome: MergeOutcome) -> (String, usize) {
match outcome {
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => (
String::from_utf8(merged_bytes_with_markers).expect("UTF-8"),
conflict_count,
),
other => panic!("expected Conflicts, got {other:?}"),
}
}
#[test]
fn min_case_1_different_functions_modified_clean_merge() {
let base = "\
fn alpha() {
println!(\"alpha-base\");
}
fn beta() {
println!(\"beta-base\");
}
";
let ours = "\
fn alpha() {
println!(\"alpha-OURS\");
}
fn beta() {
println!(\"beta-base\");
}
";
let theirs = "\
fn alpha() {
println!(\"alpha-base\");
}
fn beta() {
println!(\"beta-THEIRS\");
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("alpha-OURS"), "missing ours: {merged}");
assert!(merged.contains("beta-THEIRS"), "missing theirs: {merged}");
assert!(!merged.contains("<<<<<<<"), "no markers expected: {merged}");
}
#[test]
fn min_case_2_disjoint_new_functions_clean_merge() {
let base = "\
fn keep() {
println!(\"keep\");
}
";
let ours = "\
fn keep() {
println!(\"keep\");
}
fn ours_new() {
println!(\"ours-new\");
}
";
let theirs = "\
fn keep() {
println!(\"keep\");
}
fn theirs_new() {
println!(\"theirs-new\");
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("ours_new"), "ours_new missing: {merged}");
assert!(
merged.contains("theirs_new"),
"theirs_new missing: {merged}"
);
}
#[test]
fn min_case_3_same_function_different_lines() {
let base = "\
fn target() {
let a = 1;
let b = 2;
let c = 3;
let d = 4;
let e = 5;
}
";
let ours = "\
fn target() {
let a = 1;
let b = 2;
let c = 999;
let d = 4;
let e = 5;
}
";
let theirs = "\
fn target() {
let a = 1;
let b = 888;
let c = 3;
let d = 4;
let e = 5;
}
";
let outcome = merge_rust(base, ours, theirs);
match outcome {
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
assert!(text.contains("let b = 888"), "missing theirs: {text}");
assert!(text.contains("let c = 999"), "missing ours: {text}");
}
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => {
let text = String::from_utf8(merged_bytes_with_markers).unwrap();
assert!(conflict_count <= 2, "too many conflicts: {conflict_count}");
assert!(text.contains("let a = 1"), "verbatim line lost: {text}");
assert!(text.contains("let d = 4"), "verbatim line lost: {text}");
}
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn min_case_4_same_function_overlapping_lines() {
let base = "\
fn target() {
let value = 10;
}
fn untouched() {
println!(\"untouched\");
}
";
let ours = "\
fn target() {
let value = 20;
}
fn untouched() {
println!(\"untouched\");
}
";
let theirs = "\
fn target() {
let value = 30;
}
fn untouched() {
println!(\"untouched\");
}
";
let (text, conflict_count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(conflict_count >= 1, "expected conflict: {conflict_count}");
assert!(
text.contains("fn untouched()"),
"untouched function missing: {text}"
);
let untouched_pos = text.find("fn untouched()").unwrap();
let prefix = &text[..untouched_pos];
let opens = prefix.matches("<<<<<<<").count();
let closes = prefix.matches(">>>>>>>").count();
assert_eq!(
opens, closes,
"fn untouched() must not be inside a conflict block: {text}"
);
}
#[test]
fn min_case_5_modify_vs_delete_conflict() {
let base = "\
fn keep() {
println!(\"keep\");
}
fn target() {
let x = 1;
}
";
let ours = "\
fn keep() {
println!(\"keep\");
}
fn target() {
let x = 999;
}
";
let theirs = "\
fn keep() {
println!(\"keep\");
}
";
let outcome = merge_rust(base, ours, theirs);
match outcome {
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => {
let text = String::from_utf8(merged_bytes_with_markers).unwrap();
assert!(conflict_count >= 1, "expected a conflict");
assert!(text.contains("fn keep()"), "lost keep(): {text}");
}
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
panic!("modify/delete should NOT be clean — got: {text}");
}
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn unparseable_falls_through_to_hunk_merge() {
let base = "this is not rust @@@ #!!! \x01";
let ours = "this is not rust @@@ #!!! OURS";
let theirs = "this is not rust @@@ #!!! THEIRS";
let outcome = semantic_three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new("notrust.rs"),
MARKERS,
);
let _ = match outcome {
MergeOutcome::Clean(_) | MergeOutcome::Conflicts { .. } => true,
other => panic!("unexpected outcome: {other:?}"),
};
}
#[test]
fn unknown_language_falls_through() {
let base = "alpha\nbeta\ngamma\n";
let ours = "alpha\nBETA\ngamma\n";
let theirs = "alpha\nbeta\nGAMMA\n";
let outcome = semantic_three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new("file.xyz"),
MARKERS,
);
let merged = match outcome {
MergeOutcome::Clean(bytes) => String::from_utf8(bytes).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("BETA"), "missing ours: {merged}");
assert!(merged.contains("GAMMA"), "missing theirs: {merged}");
}
#[test]
fn impl_block_add_disjoint_methods() {
let base = "\
struct Foo;
impl Foo {
fn existing(&self) -> u32 {
0
}
}
";
let ours = "\
struct Foo;
impl Foo {
fn existing(&self) -> u32 {
0
}
fn ours_method(&self) -> u32 {
1
}
}
";
let theirs = "\
struct Foo;
impl Foo {
fn existing(&self) -> u32 {
0
}
fn theirs_method(&self) -> u32 {
2
}
}
";
let outcome = merge_rust(base, ours, theirs);
match outcome {
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
assert!(text.contains("ours_method"), "ours method missing: {text}");
assert!(
text.contains("theirs_method"),
"theirs method missing: {text}"
);
}
other => panic!("expected Clean, got {other:?}"),
}
}
#[test]
fn impl_different_methods_modified_clean() {
let base = "\
struct Foo;
impl Foo {
fn a(&self) -> u32 { 0 }
fn b(&self) -> u32 { 0 }
}
";
let ours = "\
struct Foo;
impl Foo {
fn a(&self) -> u32 { 11 }
fn b(&self) -> u32 { 0 }
}
";
let theirs = "\
struct Foo;
impl Foo {
fn a(&self) -> u32 { 0 }
fn b(&self) -> u32 { 22 }
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("11"), "missing ours mod: {merged}");
assert!(merged.contains("22"), "missing theirs mod: {merged}");
}
#[test]
fn ours_reorders_theirs_modifies_other_clean() {
let base = "\
fn one() { println!(\"1\"); }
fn two() { println!(\"2\"); }
fn three() { println!(\"3\"); }
";
let ours = "\
fn one() { println!(\"1\"); }
fn three() { println!(\"3\"); }
fn two() { println!(\"2\"); }
";
let theirs = "\
fn one() { println!(\"ONE\"); }
fn two() { println!(\"2\"); }
fn three() { println!(\"3\"); }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("ONE"), "missing theirs edit: {merged}");
assert!(merged.contains("fn two()"));
assert!(merged.contains("fn three()"));
}
#[test]
fn whitespace_only_divergence_resolves() {
let base = "fn f() {\n let a = 1;\n}\n";
let ours = "fn f() {\n let a = 1;\n}\n";
let theirs = "fn f() {\n let a = 1;\n}\n";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean: {other:?}"),
};
assert!(text.contains("fn f()"));
}
#[test]
fn both_sides_add_same_name_different_body_conflict() {
let base = "fn keep() { println!(\"k\"); }\n";
let ours = "\
fn keep() { println!(\"k\"); }
fn newcomer() { println!(\"ours-newcomer\"); }
";
let theirs = "\
fn keep() { println!(\"k\"); }
fn newcomer() { println!(\"theirs-newcomer\"); }
";
match merge_rust(base, ours, theirs) {
MergeOutcome::Conflicts { conflict_count, .. } => {
assert!(conflict_count >= 1);
}
other => panic!("expected Conflicts, got {other:?}"),
}
}
#[test]
fn both_sides_add_same_function_identical_body_clean() {
let base = "fn keep() { println!(\"k\"); }\n";
let same_addition = "\
fn keep() { println!(\"k\"); }
fn newcomer() { println!(\"same body\"); }
";
let merged = assert_clean(merge_rust(base, same_addition, same_addition));
assert!(merged.contains("newcomer"));
}
#[test]
fn structural_reshape_resolves_where_text_merge_struggles() {
let base = "\
fn a() { 1 }
fn b() { 2 }
fn c() { 3 }
fn d() { 4 }
fn e() { 5 }
";
let ours = "\
fn e() { 5 }
fn a() { 1 }
fn c() { 3 }
fn b() { 2 }
fn d() { 4 }
";
let theirs = "\
fn a() { 1 }
fn b() { 2 }
fn c() { 333 }
fn d() { 4 }
fn e() { 5 }
fn f() { 6 }
";
let outcome = merge_rust(base, ours, theirs);
let merged = assert_clean(outcome);
assert!(
merged.contains("fn c() { 333 }"),
"theirs c-edit lost: {merged}"
);
assert!(merged.contains("fn f() { 6 }"), "theirs add lost: {merged}");
for name in ["fn a", "fn b", "fn c", "fn d", "fn e"] {
assert!(merged.contains(name), "missing {name}: {merged}");
}
assert!(!merged.contains("<<<<<<<"), "no markers expected: {merged}");
}
#[test]
fn heddle_54_replay_shape_resolves_with_at_most_one_conflict() {
fn body(suffix: &str) -> String {
let mut s = String::new();
for i in 0..20 {
s.push_str(&format!("fn fn_{i}() {{ let x = {i}{suffix}; }}\n\n"));
}
s
}
let base = body("");
let mut ours = base.clone();
let mut theirs = base.clone();
for i in 0..10 {
ours = ours.replace(
&format!("fn fn_{i}() {{ let x = {i}; }}"),
&format!("fn fn_{i}() {{ let x = {i}_OURS; }}"),
);
}
for i in 10..20 {
theirs = theirs.replace(
&format!("fn fn_{i}() {{ let x = {i}; }}"),
&format!("fn fn_{i}() {{ let x = {i}_THEIRS; }}"),
);
}
let outcome = merge_rust(&base, &ours, &theirs);
let merged = assert_clean(outcome);
for i in 0..10 {
assert!(merged.contains(&format!("{i}_OURS")), "ours edit {i} lost");
}
for i in 10..20 {
assert!(
merged.contains(&format!("{i}_THEIRS")),
"theirs edit {i} lost"
);
}
}
#[test]
fn semantic_beats_text_merge_on_structural_reshape() {
let base = "\
fn a() { let x = 1; }
fn b() { let x = 2; }
fn c() { let x = 3; }
fn d() { let x = 4; }
";
let ours = "\
fn d() { let x = 4; }
fn c() { let x = 3; }
fn b() { let x = 22; }
fn a() { let x = 1; }
";
let theirs = "\
fn a() { let x = 1; }
fn b() { let x = 2; }
fn c() { let x = 3; }
fn d() { let x = 44; }
";
let sem_outcome = merge_rust(base, ours, theirs);
let sem_text = match sem_outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => {
let preview = String::from_utf8_lossy(&merged_bytes_with_markers).into_owned();
panic!("semantic driver should resolve cleanly. got:\n{preview}");
}
other => panic!("unexpected outcome: {other:?}"),
};
assert!(sem_text.contains("let x = 22"), "ours edit lost");
assert!(sem_text.contains("let x = 44"), "theirs edit lost");
let direct = merge::text_hunk_merge_with_markers(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
MARKERS,
);
let direct_markers = match direct {
MergeOutcome::Clean(_) => 0,
MergeOutcome::Conflicts { conflict_count, .. } => conflict_count,
other => panic!("unexpected outcome: {other:?}"),
};
assert!(
direct_markers > 0,
"expected text_hunk_merge to surface ≥1 conflict on this shape; \
if heddle-merge has improved, retire this comparison test"
);
}
#[test]
fn many_functions_disjoint_modifications_resolves() {
let mut base = String::new();
for i in 0..200 {
base.push_str(&format!("fn fn_{i}() {{ let x = {i}; }}\n\n"));
}
let mut ours = base.clone();
let mut theirs = base.clone();
ours = ours.replace("fn fn_50() { let x = 50; }", "fn fn_50() { let x = 5050; }");
theirs = theirs.replace(
"fn fn_150() { let x = 150; }",
"fn fn_150() { let x = 15150; }",
);
let merged = assert_clean(merge_rust(&base, &ours, &theirs));
assert!(merged.contains("5050"), "ours edit lost");
assert!(merged.contains("15150"), "theirs edit lost");
}
#[test]
fn all_three_sides_identical_returns_base() {
let s = "fn a() { 1 }\n";
let out = merge_rust(s, s, s);
assert_eq!(assert_clean(out), s);
}
#[test]
fn base_equals_ours_takes_theirs() {
let base = "fn a() { 1 }\n";
let theirs = "fn a() { 2 }\n";
let out = merge_rust(base, base, theirs);
assert_eq!(assert_clean(out), theirs);
}
#[test]
fn base_equals_theirs_takes_ours() {
let base = "fn a() { 1 }\n";
let ours = "fn a() { 2 }\n";
let out = merge_rust(base, ours, base);
assert_eq!(assert_clean(out), ours);
}
#[test]
fn ours_equals_theirs_takes_either() {
let base = "fn a() { 1 }\n";
let same = "fn a() { 99 }\n";
let out = merge_rust(base, same, same);
assert_eq!(assert_clean(out), same);
}
#[test]
fn non_utf8_input_falls_through_to_hunk_merge() {
let base: &[u8] = b"fn a() { 1 }\n\xff\n";
let ours: &[u8] = b"fn a() { 1 }\n\xff OURS\n";
let theirs: &[u8] = b"fn a() { 1 }\n\xff THEIRS\n";
let outcome = semantic_three_way_merge(base, ours, theirs, Path::new("a.rs"), MARKERS);
match outcome {
MergeOutcome::Clean(_) | MergeOutcome::Conflicts { .. } => {}
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn three_way_merge_hunk_only_strategy_skips_parsing() {
let base = "fn a() { 1 }\nfn b() { 2 }\n";
let ours = "fn a() { 10 }\nfn b() { 2 }\n";
let theirs = "fn a() { 1 }\nfn b() { 20 }\n";
let out = three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new("a.rs"),
MARKERS,
MergeStrategy::HunkOnly,
);
let merged = assert_clean(out);
assert!(merged.contains("10"));
assert!(merged.contains("20"));
}
#[test]
fn three_way_merge_semantic_strategy_uses_ast() {
let base = "fn a() { 1 }\nfn b() { 2 }\n";
let ours = "fn a() { 10 }\nfn b() { 2 }\n";
let theirs = "fn a() { 1 }\nfn b() { 20 }\n";
let out = three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new("a.rs"),
MARKERS,
MergeStrategy::Semantic,
);
let merged = assert_clean(out);
assert!(merged.contains("10"));
assert!(merged.contains("20"));
}
#[test]
fn modify_delete_clean_when_modifier_preserved_base() {
let base = "fn keep() { 1 }\nfn target() { 1 }\n";
let ours = "fn keep() { 2 }\nfn target() { 1 }\n";
let theirs = "fn keep() { 1 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
!merged.contains("fn target"),
"target should be deleted: {merged}"
);
assert!(merged.contains("fn keep() { 2 }"));
}
#[test]
fn delete_modify_clean_when_modifier_preserved_base() {
let base = "fn keep() { 1 }\nfn target() { 1 }\n";
let ours = "fn keep() { 2 }\n";
let theirs = "fn keep() { 1 }\nfn target() { 1 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(!merged.contains("fn target"));
assert!(merged.contains("fn keep() { 2 }"));
}
#[test]
fn both_sides_add_identical_function_with_other_divergence_clean() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "fn alpha() { 10 }\nfn beta() { 2 }\nfn newcomer() { 99 }\n";
let theirs = "fn alpha() { 1 }\nfn beta() { 20 }\nfn newcomer() { 99 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("fn newcomer() { 99 }"));
assert!(merged.contains("fn alpha() { 10 }"));
assert!(merged.contains("fn beta() { 20 }"));
}
#[test]
fn both_sides_delete_same_function_clean() {
let base = "fn keep() { 1 }\nfn gone() { 0 }\n";
let ours = "fn keep() { 2 }\n";
let theirs = "fn keep() { 3 }\n";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
!text.contains("fn gone"),
"gone() should be removed: {text}"
);
}
#[test]
fn delete_modify_conflicts_when_theirs_modified() {
let base = "fn keep() {}\nfn target() { 1 }\n";
let ours = "fn keep() {}\n";
let theirs = "fn keep() {}\nfn target() { 999 }\n";
let (_text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1);
}
#[test]
fn three_way_modify_ours_unchanged_takes_theirs() {
let base = "fn a() { 1 }\n";
let ours = "fn a() { 1 }\n";
let theirs = "fn a() { 42 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("42"));
}
#[test]
fn three_way_modify_both_made_same_change_takes_ours() {
let base = "fn a() { 1 }\nfn b() { 2 }\n";
let ours = "fn a() { 1 }\nfn b() { 42 }\n";
let theirs = "fn a() { 1 }\nfn b() { 42 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("42"));
}
#[test]
fn rust_struct_modified_disjoint_clean() {
let base = "struct S { x: u32 }\nfn f() { 1 }\n";
let ours = "struct S { x: u64 }\nfn f() { 1 }\n";
let theirs = "struct S { x: u32 }\nfn f() { 2 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("u64"));
assert!(merged.contains("fn f() { 2 }"));
}
#[test]
fn rust_enum_modified_disjoint_clean() {
let base = "enum E { A, B }\nfn f() { 1 }\n";
let ours = "enum E { A, B, C }\nfn f() { 1 }\n";
let theirs = "enum E { A, B }\nfn f() { 99 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("C"));
assert!(merged.contains("99"));
}
#[test]
fn rust_trait_with_signature_methods_disjoint_clean() {
let base = "\
trait T {
fn foo(&self);
fn bar(&self);
}
fn k() { 1 }
";
let ours = "\
trait T {
fn foo(&self) -> u32;
fn bar(&self);
}
fn k() { 1 }
";
let theirs = "\
trait T {
fn foo(&self);
fn bar(&self);
}
fn k() { 2 }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("-> u32"));
assert!(merged.contains("fn k() { 2 }"));
}
#[test]
fn rust_const_static_type_alias_modified_clean() {
let base = "\
const C: u32 = 1;
static S: u32 = 2;
type T = u32;
fn k() { 0 }
";
let ours = "\
const C: u32 = 10;
static S: u32 = 2;
type T = u32;
fn k() { 0 }
";
let theirs = "\
const C: u32 = 1;
static S: u32 = 20;
type T = u64;
fn k() { 0 }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("C: u32 = 10"));
assert!(merged.contains("S: u32 = 20"));
assert!(merged.contains("type T = u64"));
}
#[test]
fn rust_union_modified_clean() {
let base = "\
union U { a: u32, b: u64 }
fn k() { 0 }
";
let ours = "\
union U { a: u32, b: u128 }
fn k() { 0 }
";
let theirs = "\
union U { a: u32, b: u64 }
fn k() { 1 }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("u128"));
assert!(merged.contains("fn k() { 1 }"));
}
#[test]
fn rust_mod_header_only_change() {
let base = "mod a;\nmod b;\nfn k() { 0 }\n";
let ours = "mod a;\nmod b;\nfn k() { 1 }\n";
let theirs = "mod a;\nmod b;\nfn k() { 0 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("fn k() { 1 }"));
}
#[test]
fn rust_mod_with_inline_body_disjoint_methods_clean() {
let base = "\
mod inner {
fn one() { 1 }
fn two() { 2 }
}
";
let ours = "\
mod inner {
fn one() { 10 }
fn two() { 2 }
}
";
let theirs = "\
mod inner {
fn one() { 1 }
fn two() { 20 }
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("fn one() { 10 }"));
assert!(merged.contains("fn two() { 20 }"));
}
#[test]
fn rust_impl_trait_for_type_distinct_from_inherent_impl() {
let base = "\
struct Foo;
impl Foo {
fn inherent(&self) { let _ = 1; }
}
impl Clone for Foo {
fn clone(&self) -> Self { Foo }
}
";
let ours = "\
struct Foo;
impl Foo {
fn inherent(&self) { let _ = 42; }
}
impl Clone for Foo {
fn clone(&self) -> Self { Foo }
}
";
let theirs = "\
struct Foo;
impl Foo {
fn inherent(&self) { let _ = 1; }
}
impl Clone for Foo {
fn clone(&self) -> Self { Foo.clone() }
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("let _ = 42"));
assert!(merged.contains("Foo.clone()"));
}
fn merge_at(base: &str, ours: &str, theirs: &str, path: &str) -> MergeOutcome {
semantic_three_way_merge(
base.as_bytes(),
ours.as_bytes(),
theirs.as_bytes(),
Path::new(path),
MARKERS,
)
}
#[test]
fn python_function_disjoint_modifications_clean() {
let base = "\
def alpha():
return 1
def beta():
return 2
";
let ours = "\
def alpha():
return 11
def beta():
return 2
";
let theirs = "\
def alpha():
return 1
def beta():
return 22
";
let merged = match merge_at(base, ours, theirs, "f.py") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("return 11"));
assert!(merged.contains("return 22"));
}
#[test]
fn python_class_with_methods_disjoint_clean() {
let base = "\
class C:
def one(self):
return 1
def two(self):
return 2
";
let ours = "\
class C:
def one(self):
return 10
def two(self):
return 2
";
let theirs = "\
class C:
def one(self):
return 1
def two(self):
return 20
";
let merged = match merge_at(base, ours, theirs, "f.py") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("return 10"));
assert!(merged.contains("return 20"));
}
#[test]
fn python_pyi_extension_also_handled() {
let base = "def f():\n return 1\n";
let ours = "def f():\n return 2\n";
let theirs = base;
let outcome = merge_at(base, ours, theirs, "stub.pyi");
match outcome {
MergeOutcome::Clean(b) => {
let s = String::from_utf8(b).unwrap();
assert!(s.contains("return 2"));
}
other => panic!("expected Clean, got {other:?}"),
}
}
#[test]
fn javascript_function_declarations_disjoint_clean() {
let base = "\
function a() { return 1; }
function b() { return 2; }
";
let ours = "\
function a() { return 11; }
function b() { return 2; }
";
let theirs = "\
function a() { return 1; }
function b() { return 22; }
";
let merged = match merge_at(base, ours, theirs, "f.js") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("return 11"));
assert!(merged.contains("return 22"));
}
#[test]
fn javascript_class_methods_disjoint_clean() {
let base = "\
class C {
one() { return 1; }
two() { return 2; }
}
";
let ours = "\
class C {
one() { return 10; }
two() { return 2; }
}
";
let theirs = "\
class C {
one() { return 1; }
two() { return 20; }
}
";
let merged = match merge_at(base, ours, theirs, "f.js") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("return 10"));
assert!(merged.contains("return 20"));
}
#[test]
fn typescript_class_method_modified_clean() {
let base = "\
class C {
greet(name: string): string { return name; }
bye(): void {}
}
";
let ours = "\
class C {
greet(name: string): string { return name.toUpperCase(); }
bye(): void {}
}
";
let theirs = "\
class C {
greet(name: string): string { return name; }
bye(): void { console.log('bye'); }
}
";
let merged = match merge_at(base, ours, theirs, "f.ts") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("toUpperCase"));
assert!(merged.contains("console.log"));
}
#[test]
fn preamble_diverging_edits_surface_conflict_in_inter_item_merge() {
let base = "\
// header note: base
fn f() { 1 }
";
let ours = "\
// header note: OURS
fn f() { 1 }
";
let theirs = "\
// header note: THEIRS
fn f() { 1 }
";
let outcome = merge_rust(base, ours, theirs);
match outcome {
MergeOutcome::Conflicts { conflict_count, .. } => {
assert!(conflict_count >= 1);
}
MergeOutcome::Clean(_) => panic!("expected conflict on diverging preamble"),
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn nested_function_inside_outer_does_not_split_outer_item() {
let base = "\
fn outer() {
fn inner() { 1 }
inner()
}
";
let ours = "\
fn outer() {
fn inner() { 99 }
inner()
}
";
let theirs = base;
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("fn inner() { 99 }"));
}
#[test]
fn typescript_overload_signatures_not_collapsed_by_key_collision() {
let base = "\
function foo(x: number): number { return 1; }
function foo(x: string): string { return \"a\"; }
";
let ours = "\
function foo(x: number): number { return 100; }
function foo(x: string): string { return \"a\"; }
";
let theirs = "\
function foo(x: number): number { return 1; }
function foo(x: string): string { return \"AAA\"; }
";
let merged = match merge_at(base, ours, theirs, "f.ts") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected outcome: {other:?}"),
};
assert!(
merged.contains("x: number"),
"foo(number) overload lost: {merged}"
);
assert!(
merged.contains("x: string"),
"foo(string) overload lost: {merged}"
);
assert!(merged.contains("return 100"), "ours edit lost: {merged}");
assert!(
merged.contains("return \"AAA\""),
"theirs edit lost: {merged}"
);
}
#[test]
fn go_methods_on_different_receivers_not_collapsed() {
let base = "\
package main
type A struct{}
func (a A) String() string { return \"a\" }
type B struct{}
func (b B) String() string { return \"b\" }
";
let ours = "\
package main
type A struct{}
func (a A) String() string { return \"A-OURS\" }
type B struct{}
func (b B) String() string { return \"b\" }
";
let theirs = "\
package main
type A struct{}
func (a A) String() string { return \"a\" }
type B struct{}
func (b B) String() string { return \"B-THEIRS\" }
";
let merged = match merge_at(base, ours, theirs, "f.go") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("(a A) String()"), "A.String lost: {merged}");
assert!(merged.contains("(b B) String()"), "B.String lost: {merged}");
assert!(merged.contains("A-OURS"), "ours edit lost: {merged}");
assert!(merged.contains("B-THEIRS"), "theirs edit lost: {merged}");
}
#[test]
fn impl_block_single_line_disjoint_method_edits_merge_cleanly() {
let base = "impl A { fn x() { 0 } fn y() { 0 } }\n";
let ours = "impl A { fn x() { 11 } fn y() { 0 } }\n";
let theirs = "impl A { fn x() { 0 } fn y() { 22 } }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("fn x() { 11 }"), "ours edit lost: {merged}");
assert!(
merged.contains("fn y() { 22 }"),
"theirs edit lost: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"expected clean merge, got markers: {merged}"
);
}
#[test]
fn python_top_level_executable_statement_stays_between_functions() {
let base = "\
import x
def foo():
return 1
x.init()
def bar():
return 2
";
let ours = "\
import x
def foo():
return 11
x.init()
def bar():
return 2
";
let theirs = "\
import x
def foo():
return 1
x.init()
def bar():
return 22
";
let merged = match merge_at(base, ours, theirs, "f.py") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
let p_foo = merged.find("def foo()").expect("foo present");
let p_init = merged.find("x.init()").expect("x.init() present");
let p_bar = merged.find("def bar()").expect("bar present");
assert!(
p_foo < p_init && p_init < p_bar,
"expected foo < x.init() < bar in:\n{merged}"
);
assert!(merged.contains("return 11"), "ours edit lost: {merged}");
assert!(merged.contains("return 22"), "theirs edit lost: {merged}");
}
#[test]
fn deeply_nested_rust_modules_does_not_stack_overflow() {
let depth = 2000usize;
let mut s = String::new();
for i in 0..depth {
s.push_str(&format!("mod m{i} {{\n"));
}
s.push_str(" fn inner() { 1 }\n");
for _ in 0..depth {
s.push_str("}\n");
}
let base = s.clone();
let ours = s.replace("fn inner() { 1 }", "fn inner() { 2 }");
let theirs = s.replace("fn inner() { 1 }", "fn inner() { 1; let _ = 0; }");
let handle = std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn(move || {
merge_rust(&base, &ours, &theirs);
})
.expect("spawn");
handle
.join()
.expect("merge must not stack-overflow on deeply-nested input");
}
#[test]
fn deep_nesting_past_max_traversal_depth_falls_through_to_text() {
let depth = 300usize;
let mut s = String::new();
for i in 0..depth {
s.push_str(&format!("mod m{i} {{\n"));
}
s.push_str("fn inner() { 1 }\nfn other() { 0 }\n");
for _ in 0..depth {
s.push_str("}\n");
}
let ours = s.replace("fn inner() { 1 }", "fn inner() { 2 }");
let theirs = s.replace("fn other() { 0 }", "fn other() { 99 }");
let outcome = merge_rust(&s, &ours, &theirs);
match outcome {
MergeOutcome::Clean(_) | MergeOutcome::Conflicts { .. } => {}
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn go_free_function_modified_clean() {
let base = "\
package p
func Add(a, b int) int { return a + b }
func Sub(a, b int) int { return a - b }
";
let ours = "\
package p
func Add(a, b int) int { return a + b + 0 }
func Sub(a, b int) int { return a - b }
";
let theirs = "\
package p
func Add(a, b int) int { return a + b }
func Sub(a, b int) int { return a - b - 0 }
";
let outcome = merge_at(base, ours, theirs, "f.go");
let merged = match outcome {
MergeOutcome::Clean(bytes) => String::from_utf8(bytes).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(
merged.contains("return a + b + 0"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("return a - b - 0"),
"theirs edit lost: {merged}"
);
}
#[test]
fn go_method_pointer_receiver_keyed_distinctly_from_value_receiver() {
let base = "\
package p
type A struct{}
func (a A) M() int { return 0 }
func (a *A) M() int { return 1 }
";
let ours = "\
package p
type A struct{}
func (a A) M() int { return 10 }
func (a *A) M() int { return 1 }
";
let theirs = "\
package p
type A struct{}
func (a A) M() int { return 0 }
func (a *A) M() int { return 11 }
";
let outcome = merge_at(base, ours, theirs, "f.go");
let merged = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("return 10"));
assert!(merged.contains("return 11"));
assert!(merged.contains("(a A) M()"));
assert!(merged.contains("(a *A) M()"));
}
#[test]
fn three_adjacent_added_items_preserve_source_order() {
let base = "\
fn a() { 1 }
fn z() { 9 }
";
let ours = "\
fn a() { 1 }
fn b() { 2 }
fn c() { 3 }
fn d() { 4 }
fn z() { 9 }
";
let theirs = "\
fn a() { 1 }
fn z() { 99 }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
let pa = merged.find("fn a(").expect("a present");
let pb = merged.find("fn b(").expect("b present");
let pc = merged.find("fn c(").expect("c present");
let pd = merged.find("fn d(").expect("d present");
let pz = merged.find("fn z(").expect("z present");
assert!(
pa < pb && pb < pc && pc < pd && pd < pz,
"expected a < b < c < d < z order in:\n{merged}"
);
}
#[test]
fn javascript_top_level_same_name_functions_distinguishable_by_arity() {
let base = "\
function foo(x) { return x; }
function foo(x, y) { return x + y; }
";
let ours = "\
function foo(x) { return x + 10; }
function foo(x, y) { return x + y; }
";
let theirs = "\
function foo(x) { return x; }
function foo(x, y) { return x * y; }
";
let merged = match merge_at(base, ours, theirs, "f.js") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(
merged.contains("function foo(x)") && merged.contains("function foo(x, y)"),
"one of the arities lost: {merged}"
);
assert!(merged.contains("return x + 10"), "ours edit lost: {merged}");
assert!(
merged.contains("return x * y"),
"theirs edit lost: {merged}"
);
}
#[test]
fn signature_hash_canonicalizes_punctuation_so_formatting_only_change_matches() {
let base = "\
fn foo(x: u32, y: u32) -> u32 {
0
}
";
let ours = "\
fn foo(x: u32,y: u32) -> u32 {
1
}
";
let theirs = "\
fn foo(x: u32, y: u32) -> u32 {
2
}
";
let merged = match merge_rust(base, ours, theirs) {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let close_brace_count = merged.matches('}').count();
assert_eq!(
close_brace_count, 1,
"expected ONE foo definition (signature hash must canonicalize \
formatting-only param changes), got {close_brace_count} closing \
braces: {merged}"
);
}
#[test]
fn go_receiver_type_canonicalizes_whitespace_around_pointer_star() {
let base = "\
package p
type A struct{}
func (a *A) M() int {
return 0
}
";
let ours = "\
package p
type A struct{}
func (a * A) M() int {
return 1
}
";
let theirs = "\
package p
type A struct{}
func (a *A) M() int {
return 2
}
";
let merged = match merge_at(base, ours, theirs, "f.go") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let close_count = merged.matches("\n}").count();
assert_eq!(
close_count, 1,
"expected ONE M() definition (receiver type must canonicalize \
whitespace), got {close_count} line-leading closing braces: {merged}"
);
}
#[test]
fn no_base_items_both_sides_add_different_items_preamble_not_duplicated() {
let base = "// top header\n";
let ours = "\
// top header
use std::a;
fn alpha() { 1 }
";
let theirs = "\
// top header
use std::b;
fn beta() { 2 }
";
let merged = match merge_rust(base, ours, theirs) {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let header_count = merged.matches("// top header").count();
assert_eq!(
header_count, 1,
"expected `// top header` exactly once, got {header_count}: {merged}"
);
}
#[test]
fn zero_items_side_postamble_does_not_duplicate_bridging_segment() {
let base = "fn a() { 1 }\n";
let ours = "// lone comment\n";
let theirs = "fn a() { 2 }\n";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let use_count = text.matches("// lone comment").count();
assert_eq!(
use_count, 1,
"expected ours's `// lone comment` exactly once, got {use_count}: {text}"
);
}
#[test]
fn deletion_with_opposite_side_surrounding_edits_preserved_at_correct_positions() {
let base = "\
import x
def foo():
pass
# trailing comment
";
let ours = "\
import x
# trailing comment
";
let theirs = "\
import y
def foo():
pass
# trailing y
";
let outcome = merge_at(base, ours, theirs, "f.py");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(text.contains("import y"), "import edit lost: {text}");
assert!(text.contains("# trailing y"), "trailing edit lost: {text}");
assert!(!text.contains("def foo"), "foo should be deleted: {text}");
let pos_import = text.find("import y").expect("import present");
let pos_trailing = text.find("# trailing y").expect("trailing present");
assert!(
pos_import < pos_trailing,
"trailing edit shifted ahead of import: {text}"
);
assert!(
!text.contains("import x"),
"stale base import x present (theirs's edit got dropped): {text}"
);
assert!(
!text.contains("# trailing comment"),
"stale base trailing comment present (theirs's edit got dropped): {text}"
);
}
#[test]
fn no_trailing_newline_on_any_side_preserves_no_trailing_newline() {
let base = "fn foo() { 1 }";
let ours = "fn foo() { 1 }\nfn bar() {}";
let theirs = "fn foo() { 2 }";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
!text.ends_with('\n'),
"expected no trailing newline, got bytes ending {:?}: {text}",
text.as_bytes().last()
);
}
#[test]
fn rust_impl_name_ignores_whitespace_around_path_punctuation() {
let base = "\
impl std::vec::Vec<T> {
fn alpha() { 1 }
}
";
let ours = "\
impl std :: vec :: Vec < T > {
fn alpha() { 1 }
}
";
let theirs = "\
impl std::vec::Vec<T> {
fn alpha() { 2 }
}
";
let outcome = merge_rust(base, ours, theirs);
let text = assert_clean(outcome);
assert!(text.contains("{ 2 }"), "alpha body should be `2`: {text}");
let alpha_count = text.matches("fn alpha").count();
assert_eq!(
alpha_count, 1,
"expected fn alpha exactly once, got {alpha_count}: {text}"
);
}
#[test]
fn rust_impl_name_ignores_whitespace_around_pointer_punctuation() {
let base = "\
struct Foo;
impl MyTrait for *const Foo {
fn alpha() { 1 }
}
";
let ours = "\
struct Foo;
impl MyTrait for * const Foo {
fn alpha() { 1 }
}
";
let theirs = "\
struct Foo;
impl MyTrait for *const Foo {
fn alpha() { 2 }
}
";
let outcome = merge_rust(base, ours, theirs);
let text = assert_clean(outcome);
assert!(text.contains("{ 2 }"), "alpha body should be `2`: {text}");
let alpha_count = text.matches("fn alpha").count();
assert_eq!(
alpha_count, 1,
"expected fn alpha exactly once, got {alpha_count}: {text}"
);
}
#[test]
fn rust_outer_attribute_does_not_duplicate_when_adjacent_item_deleted() {
let base = "\
fn alpha() {}
#[test]
fn foo() { 1 }
";
let ours = "\
#[test]
fn foo() { 1 }
";
let theirs = "\
fn alpha() {}
#[test]
fn foo() { 2 }
";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let attr_count = text.matches("#[test]").count();
assert_eq!(
attr_count, 1,
"expected #[test] exactly once, got {attr_count}: {text}"
);
assert!(
!text.contains("fn alpha"),
"alpha should be deleted: {text}"
);
assert!(
text.contains("fn foo() { 2 }"),
"foo body should reflect theirs: {text}"
);
}
#[test]
fn java_standalone_comment_with_blank_line_does_not_move_with_next_method() {
let base = "\
class C {
// standalone
void foo() {}
void bar() {}
}
";
let ours = "\
class C {
// standalone
void bar() {}
}
";
let theirs = "\
class C {
// standalone
void foo() {}
void bar() { return; }
}
";
let outcome = merge_at(base, ours, theirs, "C.java");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let comment_count = text.matches("// standalone").count();
assert_eq!(
comment_count, 1,
"`// standalone` must survive exactly once: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"merge must be clean — pre-fix, ours's bar absorbed the comment (no blank-line gate) so the modify/modify on bar surfaces as a conflict; post-fix the comment stays in inter-item content and bar merges cleanly: {text}"
);
}
#[test]
fn python_decorated_function_delete_drops_theirs_decorator_swap() {
let base = "\
@cache
def alpha():
return 1
def beta():
return 2
";
let ours = "\
def beta():
return 2
";
let theirs = "\
@cached_property
def alpha():
return 1
def beta():
return 2
";
let outcome = merge_at(base, ours, theirs, "f.py");
let (text, has_conflicts) = match outcome {
MergeOutcome::Clean(b) => (String::from_utf8(b).unwrap(), false),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => (String::from_utf8(merged_bytes_with_markers).unwrap(), true),
other => panic!("unexpected: {other:?}"),
};
let _ = has_conflicts;
assert!(
text.contains("def alpha"),
"theirs kept `def alpha` (with new decorator); it must not vanish silently: {text}"
);
}
#[test]
fn javascript_duplicate_function_declarations_both_survive_merge() {
let base = "\
function foo() { return 1; }
function foo() { return 2; }
";
let ours = "\
function foo() { return 1; }
function foo() { return 22; }
";
let theirs = "\
function foo() { return 11; }
function foo() { return 2; }
";
let outcome = merge_at(base, ours, theirs, "f.js");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let foo_count = text.matches("function foo").count();
assert_eq!(
foo_count, 2,
"both `function foo` declarations must survive, got {foo_count}: {text}"
);
assert!(
text.contains("return 11"),
"first declaration must show theirs's modification (return 11): {text}"
);
assert!(
text.contains("return 22"),
"second declaration must show ours's modification (return 22): {text}"
);
}
#[test]
fn rust_inner_attribute_stays_at_crate_scope_when_added_item_conflicts() {
let base = "\
#![no_std]
";
let ours = "\
#![no_std]
fn foo() { 1 }
";
let theirs = "\
#![no_std]
fn foo() { 2 }
";
let outcome = merge_rust(base, ours, theirs);
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
let attr_count = text.matches("#![no_std]").count();
assert_eq!(
attr_count, 1,
"#![no_std] must appear exactly once at crate scope, got {attr_count}: {text}"
);
}
#[test]
fn add_add_same_function_in_empty_base_surfaces_conflict_not_concatenation() {
let base = "";
let ours = "\
fn foo() {
1
}
";
let theirs = "\
fn foo() {
2
}
";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected ≥1 conflict, got {count}: {text}");
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers around foo: {text}"
);
}
#[test]
fn add_add_container_disjoint_children_no_base_conflicts() {
let base = "";
let ours = "mod foo {\n fn a() {}\n}\n";
let theirs = "mod foo {\n fn b() {}\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected ≥1 conflict, got {count}: {text}");
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers around the divergent module: {text}"
);
assert!(
text.contains("fn a()") && text.contains("fn b()"),
"both sides' bodies must survive inside the conflict: {text}"
);
assert_eq!(
text.matches("mod foo {").count(),
2,
"expected one `mod foo {{` per conflict side, not a duplicated delimiter: {text}"
);
}
#[test]
fn add_add_container_identical_no_base_takes_one() {
let base = "";
let ours = "mod foo {\n fn a() {}\n}\n";
let theirs = "mod foo {\n fn a() {}\n}\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("mod foo").count(),
1,
"module duplicated: {merged}"
);
assert_eq!(
merged.matches("fn a()").count(),
1,
"body duplicated: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"no spurious conflict on identical add/add containers: {merged}"
);
}
#[test]
fn add_add_container_divergent_header_no_base_conflicts() {
let base = "";
let ours = "pub mod foo {\n fn a() {}\n}\n";
let theirs = "mod foo {\n fn a() {}\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"expected ≥1 conflict on the divergent header, got {count}: {text}"
);
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers around the divergent module: {text}"
);
assert!(
text.contains("pub mod foo") && text.contains("mod foo {"),
"both header spellings must survive inside the conflict: {text}"
);
assert!(
!matches!(merge_rust(base, ours, theirs), MergeOutcome::Clean(_)),
"divergent add/add must not be a silent clean concat"
);
}
#[test]
fn add_add_container_shared_child_divergent_body_no_base_conflicts() {
let base = "";
let ours = "mod foo {\n fn shared() {}\n fn a() {}\n}\n";
let theirs = "mod foo {\n fn shared() {}\n fn b() {}\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected ≥1 conflict, got {count}: {text}");
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers: {text}"
);
assert!(
text.contains("fn a()") && text.contains("fn b()"),
"both divergent bodies must survive inside the conflict: {text}"
);
}
#[test]
fn base_anchored_container_merges_structurally_clean() {
let base = "mod foo {\n fn a() {}\n}\n";
let ours = "mod foo {\n fn a() {}\n fn b() {}\n}\n";
let theirs = "mod foo {\n fn a() {}\n fn c() {}\n}\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("mod foo").count(),
1,
"module duplicated: {merged}"
);
assert_eq!(
merged.matches("mod foo {").count(),
1,
"delimiter duplicated: {merged}"
);
assert!(
merged.contains("fn a()") && merged.contains("fn b()") && merged.contains("fn c()"),
"base child + both disjoint additions must weave into the single module: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"no spurious conflict on a base-anchored disjoint-additions merge: {merged}"
);
}
#[test]
fn add_add_divergent_mod_no_base_conflicts_not_duplicate_module() {
let base = "pub fn a() {}\n";
let ours = "pub fn a() {}\n\npub mod m {\n pub use crate::x;\n}\n";
let theirs = "pub fn a() {}\n\npub mod m {\n pub use crate::y;\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected ≥1 conflict, got {count}: {text}");
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers: {text}"
);
assert!(
text.contains("pub use crate::x") && text.contains("pub use crate::y"),
"both sides' re-exports must survive inside the conflict: {text}"
);
}
#[test]
fn empty_base_container_both_sides_add_weaves_single_delimiter_clean() {
let base = "mod foo {}\n";
let ours = "mod foo {\n fn a() {}\n}\n";
let theirs = "mod foo {\n fn b() {}\n}\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("mod foo {").count(),
1,
"opening delimiter duplicated: {merged}"
);
assert!(
merged.contains("fn a()") && merged.contains("fn b()"),
"both disjoint additions must weave into the single module: {merged}"
);
assert_eq!(
merged.matches('{').count(),
merged.matches('}').count(),
"braces must balance: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"no spurious conflict on disjoint additions into an empty base body: {merged}"
);
}
#[test]
fn empty_base_container_divergent_header_conflict_is_well_formed() {
let base = "mod foo {}\n";
let ours = "pub mod foo {\n fn a() {}\n}\n";
let theirs = "pub(crate) mod foo {\n fn b() {}\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"expected ≥1 conflict on the divergent header: {text}"
);
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers: {text}"
);
assert_eq!(
text.matches('{').count(),
text.matches('}').count(),
"delimiter duplicated — braces unbalanced (the r6 malformed-conflict bug): {text}"
);
for side in resolve_both_sides(&text) {
assert_eq!(
side.matches('{').count(),
side.matches('}').count(),
"a resolved side has unbalanced braces: {side}"
);
assert!(
crate::parser::ParsedFile::parse(side.as_str(), crate::parser::Language::Rust)
.is_some(),
"a resolved side does not parse: {side}"
);
}
}
#[test]
fn conflict_output_resolved_sides_always_reparse() {
let base = "mod foo {}\n";
let ours = "pub mod foo {\n fn a() {}\n}\n";
let theirs = "pub(crate) mod foo {\n fn b() {}\n}\n";
let text = match merge_rust(base, ours, theirs) {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
for side in resolve_both_sides(&text) {
assert!(
crate::parser::ParsedFile::parse(side.as_str(), crate::parser::Language::Rust)
.is_some(),
"driver emitted a conflict whose resolved side does not parse: {side}"
);
}
}
#[test]
fn extended_floor_rejects_malformed_conflict_passes_well_formed() {
use crate::parser::Language::Rust;
let malformed = "\
<<<<<<< OURS
pub mod foo
=======
pub(crate) mod foo
>>>>>>> THEIRS
{
fn b() {}{
fn a() {}
}
";
assert!(
!super::conflict_well_formed(malformed.as_bytes(), Rust),
"extended floor must reject a duplicate-delimiter conflict"
);
let well_formed = "\
<<<<<<< OURS
fn foo() { 1 }
=======
fn foo() { 2 }
>>>>>>> THEIRS
";
assert!(
super::conflict_well_formed(well_formed.as_bytes(), Rust),
"extended floor must accept a well-formed conflict whose sides both parse"
);
let broken_markers = "fn a() {}\n=======\nfn b() {}\n";
assert!(
!super::conflict_well_formed(broken_markers.as_bytes(), Rust),
"extended floor must reject structurally broken conflict markers"
);
}
#[test]
fn resolve_conflict_sides_recognizes_indented_markers() {
let start = "<<<<<<<";
let sep = "=======";
let end = ">>>>>>>";
let text = format!(
"\
mod m {{
{start} OURS
fn f() {{
{sep}
fn f() {{}}
{end} THEIRS
}}
"
);
let (ours, theirs) =
super::resolve_conflict_sides(&text).expect("indented conflict markers must resolve");
assert!(
!ours.contains("<<<<<<<") && !ours.contains("=======") && !ours.contains(">>>>>>>"),
"ours side must not retain conflict markers:\n{ours}"
);
assert!(
!theirs.contains("<<<<<<<") && !theirs.contains("=======") && !theirs.contains(">>>>>>>"),
"theirs side must not retain conflict markers:\n{theirs}"
);
assert!(
!super::conflict_well_formed(text.as_bytes(), crate::parser::Language::Rust),
"indented markers must route through structural side parsing, where the broken ours side fails"
);
}
#[test]
fn resolve_conflict_sides_does_not_treat_marker_like_data_as_markers() {
let start = "<<<<<<<";
let sep = "=======";
let end = ">>>>>>>";
let text = format!(
"\
fn f() {{
{start}not a marker
{sep}not a marker
{end}not a marker
}}
"
);
let (ours, theirs) =
super::resolve_conflict_sides(&text).expect("marker-like content must remain content");
assert_eq!(ours, text);
assert_eq!(theirs, text);
}
#[test]
fn resolve_conflict_sides_preserves_column_zero_conflicts() {
let start = "<<<<<<<";
let sep = "=======";
let end = ">>>>>>>";
let text = format!(
"\
{start} OURS
fn f() {{ 1 }}
{sep}
fn f() {{ 2 }}
{end} THEIRS
"
);
let (ours, theirs) =
super::resolve_conflict_sides(&text).expect("column-zero conflict must resolve");
assert_eq!(ours, "fn f() { 1 }\n");
assert_eq!(theirs, "fn f() { 2 }\n");
}
fn resolve_both_sides(text: &str) -> [String; 2] {
let (ours, theirs) =
super::resolve_conflict_sides(text).expect("conflict markers must resolve in test output");
[ours, theirs]
}
#[test]
fn rust_parameter_rename_does_not_split_function_identity() {
let base = "\
fn foo(x: u32) -> u32 {
let r = x + 1;
r
}
";
let ours = "\
fn foo(y: u32) -> u32 {
let r = y + 1;
r
}
";
let theirs = "\
fn foo(x: u32) -> u32 {
let r = x + 1;
r + 0
}
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.contains("fn foo(y: u32)"),
"ours's parameter rename lost (signature_hash must ignore parameter names): {merged}"
);
assert!(
merged.contains("let r = y + 1"),
"ours's body update of the renamed parameter lost: {merged}"
);
assert!(
merged.contains("r + 0"),
"theirs's body edit lost: {merged}"
);
let foo_count = merged.matches("fn foo").count();
assert_eq!(
foo_count, 1,
"expected ONE fn foo (parameter rename must not split identity), got {foo_count}: {merged}"
);
}
#[test]
fn cpp_templated_out_of_class_methods_keyed_by_their_own_name_not_template_scope() {
let base = "\
template <typename U>
void Foo<U>::bar() { int x = 0; (void)x; }
template <typename U>
void Foo<U>::foo() { int y = 0; (void)y; }
";
let ours = "\
template <typename U>
void Foo<U>::bar() { int x = 0; (void)x; }
template <typename U>
void Foo<U>::baz() { int z = 0; (void)z; }
template <typename U>
void Foo<U>::foo() { int y = 0; (void)y; }
";
let theirs = "\
template <typename U>
void Foo<U>::bar() { int x = 0; (void)x; }
template <typename U>
void Foo<U>::foo() { int y = 0; (void)y; int yy = y; (void)yy; }
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("Foo<U>::baz()"),
"ours's added Foo<U>::baz() must appear: {text}"
);
assert!(
text.contains("int z = 0"),
"Foo<U>::baz's body must survive verbatim: {text}"
);
assert!(
text.contains("int yy = y"),
"theirs's edit on Foo<U>::foo must survive: {text}"
);
let bar_count = text.matches("Foo<U>::bar()").count();
let foo_count = text.matches("Foo<U>::foo()").count();
let baz_count = text.matches("Foo<U>::baz()").count();
assert_eq!(
bar_count, 1,
"Foo<U>::bar() must appear exactly once, got {bar_count}: {text}"
);
assert_eq!(
foo_count, 1,
"Foo<U>::foo() must appear exactly once, got {foo_count}: {text}"
);
assert_eq!(
baz_count, 1,
"Foo<U>::baz() must appear exactly once, got {baz_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"merge must be clean — disjoint method edits + a clean addition: {text}"
);
}
#[test]
fn typescript_decorator_attaches_to_added_method_via_leading_metadata() {
let base = "\
class C {
foo() {}
bar() {}
}
";
let ours = "\
class C {
foo() {}
@Get()
middle() {}
bar() {}
}
";
let theirs = "\
class C {
foo() {}
@Post()
other() {}
bar() {}
}
";
let outcome = merge_at(base, ours, theirs, "f.ts");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("middle()"),
"ours's middle() must land: {text}"
);
assert!(
text.contains("other()"),
"theirs's other() must land: {text}"
);
let get_count = text.matches("@Get(").count();
let post_count = text.matches("@Post(").count();
assert_eq!(
get_count, 1,
"@Get() must appear exactly once (attached to middle), got {get_count}: {text}"
);
assert_eq!(
post_count, 1,
"@Post() must appear exactly once (attached to other), got {post_count}: {text}"
);
let get_idx = text.find("@Get").expect("@Get present");
let middle_idx = text.find("middle()").expect("middle present");
let post_idx = text.find("@Post").expect("@Post present");
let other_idx = text.find("other()").expect("other present");
assert!(get_idx < middle_idx, "@Get must precede middle: {text}");
assert!(post_idx < other_idx, "@Post must precede other: {text}");
let between_get_and_middle = &text[get_idx..middle_idx];
assert!(
!between_get_and_middle.contains("other") && !between_get_and_middle.contains("bar"),
"@Get must bind directly to middle, found stray tokens: {between_get_and_middle:?}"
);
let between_post_and_other = &text[post_idx..other_idx];
assert!(
!between_post_and_other.contains("middle") && !between_post_and_other.contains("bar"),
"@Post must bind directly to other, found stray tokens: {between_post_and_other:?}"
);
assert!(
!text.contains("<<<<<<<"),
"merge must be clean — disjoint additions of decorated methods: {text}"
);
}
#[test]
fn crlf_trailing_pair_popped_as_unit_when_majority_has_no_trailing_newline() {
let base = "fn foo() {}";
let ours = "fn foo() { 1 }";
let theirs = "fn foo() {}\r\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
!merged.ends_with('\r'),
"merged output must not end with a dangling \\r (CRLF must pop as a unit): {merged:?}"
);
assert!(
merged.ends_with('}'),
"merged output should end at the closing brace (majority wants no trailing newline): {merged:?}"
);
}
#[test]
fn cpp_same_named_methods_in_different_classes_keep_distinct_identities() {
let base = "\
class A { public: void foo(); };
void A::foo() { int a = 0; (void)a; }
";
let ours = "\
class A { public: void foo(); };
class B { public: void foo(); };
void B::foo() { int b = 99; (void)b; }
void A::foo() { int a = 0; (void)a; }
";
let theirs = "\
class A { public: void foo(); };
void A::foo() { int a = 2; (void)a; }
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("int a = 2"),
"theirs's A::foo edit must survive: {text}"
);
assert!(
text.contains("int b = 99"),
"ours's added B::foo body must survive: {text}"
);
let a_def_count = text.matches("void A::foo()").count();
let b_def_count = text.matches("void B::foo()").count();
assert_eq!(
a_def_count, 1,
"A::foo definition must appear exactly once, got {a_def_count}: {text}"
);
assert_eq!(
b_def_count, 1,
"B::foo definition must appear exactly once, got {b_def_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint cross-class edits + addition must merge cleanly: {text}"
);
}
#[test]
fn cpp_pointer_overload_distinct_from_value_overload() {
let base = "\
void f(int) { int x = 0; (void)x; }
";
let ours = "\
void f(int* p) { int y = *p; (void)y; }
void f(int) { int x = 0; (void)x; }
";
let theirs = "\
void f(int) { int x = 99; (void)x; }
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("int x = 99"),
"theirs's edit on f(int) must survive: {text}"
);
assert!(
text.contains("int y = *p"),
"ours's added f(int*) body must survive: {text}"
);
let value_overload = text.matches("void f(int)").count();
let ptr_overload = text.matches("void f(int* p)").count();
assert_eq!(
value_overload, 1,
"void f(int) must appear exactly once, got {value_overload}: {text}"
);
assert_eq!(
ptr_overload, 1,
"void f(int* p) must appear exactly once, got {ptr_overload}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint overload addition + body edit must merge cleanly: {text}"
);
}
#[test]
fn crlf_add_add_conflict_markers_use_crlf() {
let base = "// header\r\n";
let ours = "// header\r\nfn foo() {\r\n 1\r\n}\r\n";
let theirs = "// header\r\nfn foo() {\r\n 2\r\n}\r\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert_eq!(count, 1, "expected 1 conflict, got {count}: {text:?}");
for line in text.split_inclusive('\n') {
if line.starts_with("<<<<<<<") || line.starts_with("=======") || line.starts_with(">>>>>>>")
{
assert!(
line.ends_with("\r\n"),
"marker line `{}` must end with CRLF in a CRLF file: {text:?}",
line.trim_end_matches('\n').trim_end_matches('\r'),
);
}
}
let bytes = text.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b'\n' {
assert!(
i > 0 && bytes[i - 1] == b'\r',
"bare LF at byte {i} in otherwise-CRLF output: {text:?}"
);
}
}
}
#[test]
fn mixed_eol_add_add_marker_follows_item_bytes_not_whole_file() {
let pad = "// pad 1\n// pad 2\n// pad 3\n// pad 4\n// pad 5\n// pad 6\n// pad 7\n// pad 8\n";
let base = format!("fn bar() {{}}\n\n{pad}\n");
let ours = format!("fn bar() {{}}\n\n{pad}\nfn foo() {{\r\n 1\r\n}}\r\n");
let theirs = format!("fn bar() {{}}\n\n{pad}\nfn foo() {{\r\n 2\r\n}}\r\n");
let base = base.as_str();
let ours = ours.as_str();
let theirs = theirs.as_str();
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert_eq!(count, 1, "expected 1 conflict, got {count}: {text:?}");
for line in text.split_inclusive('\n') {
if line.starts_with("<<<<<<<") || line.starts_with("=======") || line.starts_with(">>>>>>>")
{
assert!(
line.ends_with("\r\n"),
"marker line `{}` must use CRLF to match the CRLF item bodies it wraps, \
even though the surrounding file is majority-LF: {text:?}",
line.trim_end_matches('\n').trim_end_matches('\r'),
);
}
}
}
#[test]
fn reconcile_trailing_newline_add_case_uses_crlf_when_file_is_crlf() {
let base = "fn foo() {}\r\n";
let ours = "fn foo() {}\r\nfn bar() {}";
let theirs = "fn foo() { 1 }\r\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.ends_with("\r\n"),
"merged output must end with CRLF when the file is CRLF (ADD case): {merged:?}"
);
let bytes = merged.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b'\n' {
assert!(
i > 0 && bytes[i - 1] == b'\r',
"bare LF at byte {i} in otherwise-CRLF output: {merged:?}"
);
}
}
}
#[test]
fn typescript_optional_parameter_distinct_from_required_parameter() {
let base = "\
function foo(x: number): number {
return x + 0;
}
";
let ours = "\
function foo(x?: number): number {
return (x ?? 0) + 1;
}
function foo(x: number): number {
return x + 0;
}
";
let theirs = "\
function foo(x: number): number {
return x + 999;
}
";
let outcome = merge_at(base, ours, theirs, "f.ts");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("return x + 999"),
"theirs's edit on foo(x: number) must survive: {text}"
);
assert!(
text.contains("return (x ?? 0) + 1"),
"ours's added foo(x?: number) body must survive: {text}"
);
let required_count = text.matches("foo(x: number)").count();
let optional_count = text.matches("foo(x?: number)").count();
assert_eq!(
required_count, 1,
"foo(x: number) must appear exactly once, got {required_count}: {text}"
);
assert_eq!(
optional_count, 1,
"foo(x?: number) must appear exactly once, got {optional_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint overload addition + body edit must merge cleanly: {text}"
);
}
#[test]
fn cpp_inline_same_named_methods_in_different_classes_keep_distinct_identities() {
let base = "\
class A {
void foo() { int a = 0; (void)a; }
};
";
let ours = "\
class B {
void foo() { int b = 99; (void)b; }
};
class A {
void foo() { int a = 0; (void)a; }
};
";
let theirs = "\
class A {
void foo() { int a = 2; (void)a; }
};
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
eprintln!("DEBUG merge result:\n{text}");
assert!(
text.contains("int a = 2"),
"theirs's A's foo edit must survive: {text}"
);
assert!(
text.contains("int b = 99"),
"ours's added B::foo body must survive: {text}"
);
let a_body_count = text.matches("int a = 2").count();
let b_body_count = text.matches("int b = 99").count();
assert_eq!(
a_body_count, 1,
"A's foo body must appear exactly once, got {a_body_count}: {text}"
);
assert_eq!(
b_body_count, 1,
"B's foo body must appear exactly once, got {b_body_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint cross-class inline edits + addition must merge cleanly: {text}"
);
}
#[test]
fn cpp_const_qualified_overload_distinct_from_unqualified() {
let base = "\
class A {
int foo() { return 0; }
};
";
let ours = "\
class A {
int foo() const { return 1; }
int foo() { return 0; }
};
";
let theirs = "\
class A {
int foo() { return 99; }
};
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("return 99"),
"theirs's edit on int foo() must survive: {text}"
);
assert!(
text.contains("return 1"),
"ours's added int foo() const body must survive: {text}"
);
let const_count = text.matches("int foo() const").count();
let unqual_count = text.matches("int foo()").count() - const_count;
assert_eq!(
const_count, 1,
"int foo() const must appear exactly once, got {const_count}: {text}"
);
assert_eq!(
unqual_count, 1,
"int foo() (unqualified) must appear exactly once, got {unqual_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint cv-qualifier overload addition + body edit must merge cleanly: {text}"
);
}
#[test]
fn typescript_interface_method_reorder_merges_cleanly() {
let base = "\
interface Foo {
a(): void;
b(): void;
c(): void;
d(): void;
e(): void;
f(): void;
}
";
let ours = "\
interface Foo {
f(): void;
e(): void;
d(): void;
c(): void;
b(): void;
a(): void;
}
";
let theirs = "\
interface Foo {
a(x: number): void;
b(): void;
c(): void;
d(): void;
e(): void;
f(y: string): void;
}
";
match merge_at(base, ours, theirs, "f.ts") {
MergeOutcome::Clean(b) => {
let text = String::from_utf8(b).unwrap();
assert!(
crate::parser::ParsedFile::parse(
text.as_str(),
crate::parser::Language::TypeScript
)
.is_some(),
"clean merge must re-parse (no silent corruption): {text}"
);
assert!(
text.contains("a(x: number)"),
"theirs's edit on a lost: {text}"
);
assert!(
text.contains("f(y: string)"),
"theirs's edit on f lost: {text}"
);
for m in ["a(", "b(", "c(", "d(", "e(", "f("] {
assert_eq!(text.matches(m).count(), 1, "{m} not once: {text}");
}
}
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => {
let text = String::from_utf8(merged_bytes_with_markers).unwrap();
assert!(
text.contains("a(x: number)"),
"theirs's edit on a lost: {text}"
);
assert!(
text.contains("f(y: string)"),
"theirs's edit on f lost: {text}"
);
for m in ["a(", "b(", "c(", "d(", "e(", "f("] {
assert!(text.contains(m), "{m} silently dropped: {text}");
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn typescript_abstract_class_method_reorder_merges_cleanly() {
let base = "\
abstract class Foo {
abstract a(): void;
abstract b(): void;
abstract c(): void;
abstract d(): void;
abstract e(): void;
abstract f(): void;
}
";
let ours = "\
abstract class Foo {
abstract f(): void;
abstract e(): void;
abstract d(): void;
abstract c(): void;
abstract b(): void;
abstract a(): void;
}
";
let theirs = "\
abstract class Foo {
abstract a(x: number): void;
abstract b(): void;
abstract c(): void;
abstract d(): void;
abstract e(): void;
abstract f(y: string): void;
}
";
let methods = [
"abstract a(",
"abstract b(",
"abstract c(",
"abstract d(",
"abstract e(",
"abstract f(",
];
match merge_at(base, ours, theirs, "f.ts") {
MergeOutcome::Clean(b) => {
let text = String::from_utf8(b).unwrap();
assert!(
crate::parser::ParsedFile::parse(
text.as_str(),
crate::parser::Language::TypeScript
)
.is_some(),
"clean merge must re-parse (no silent corruption): {text}"
);
assert!(
text.contains("abstract a(x: number)"),
"abstract a edit lost: {text}"
);
assert!(
text.contains("abstract f(y: string)"),
"abstract f edit lost: {text}"
);
for m in methods {
assert_eq!(text.matches(m).count(), 1, "{m} not once: {text}");
}
}
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => {
let text = String::from_utf8(merged_bytes_with_markers).unwrap();
assert!(
text.contains("abstract a(x: number)"),
"abstract a edit lost: {text}"
);
assert!(
text.contains("abstract f(y: string)"),
"abstract f edit lost: {text}"
);
for m in methods {
assert!(text.contains(m), "{m} silently dropped: {text}");
}
}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn detect_eol_uses_majority_when_two_of_three_inputs_are_lf() {
let base = "fn foo() {}\n";
let ours = "fn foo() {}\r\nfn bar() {}";
let theirs = "fn foo() { 1 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.ends_with('\n') && !merged.ends_with("\r\n"),
"merged output must end with LF (majority of inputs are LF): {merged:?}"
);
}
#[test]
fn addadd_conflict_markers_use_crlf_for_single_line_items_in_crlf_file() {
let base = "// header\r\n\r\n";
let ours = "// header\r\n\r\nfn foo() { 1 }\r\n";
let theirs = "// header\r\n\r\nfn foo() { 2 }\r\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert_eq!(count, 1, "expected 1 conflict, got {count}: {text:?}");
for line in text.split_inclusive('\n') {
if line.starts_with("<<<<<<<") || line.starts_with("=======") || line.starts_with(">>>>>>>")
{
assert!(
line.ends_with("\r\n"),
"marker line `{}` must end with CRLF in a CRLF file: {text:?}",
line.trim_end_matches('\n').trim_end_matches('\r'),
);
}
}
let bytes = text.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b'\n' {
assert!(
i > 0 && bytes[i - 1] == b'\r',
"bare LF at byte {i} in otherwise-CRLF output: {text:?}"
);
}
}
}
#[test]
fn cpp_noexcept_addition_does_not_split_function_identity() {
let base = "\
void foo() {
int a = 0;
(void)a;
}
";
let ours = "\
void foo() noexcept {
int a = 0;
(void)a;
}
";
let theirs = "\
void foo() {
int a = 99;
(void)a;
}
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("noexcept"),
"ours's noexcept addition must survive: {text}"
);
assert!(
text.contains("int a = 99"),
"theirs's body edit must survive: {text}"
);
let foo_signature_count =
text.matches("foo() noexcept").count() + text.matches("foo() {").count();
assert_eq!(
foo_signature_count, 1,
"foo definition must appear exactly once across the noexcept addition: got {foo_signature_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"noexcept addition + disjoint body edit must merge cleanly: {text}"
);
}
#[test]
fn cpp_noexcept_clause_with_param_name_survives_pure_rename() {
let base = "\
struct S { void bar() {} };
void f(S x) noexcept(noexcept(x.bar())) {
int a = 0;
(void)a;
}
";
let ours = "\
struct S { void bar() {} };
void f(S y) noexcept(noexcept(y.bar())) {
int a = 0;
(void)a;
}
";
let theirs = "\
struct S { void bar() {} };
void f(S x) noexcept(noexcept(x.bar())) {
int a = 99;
(void)a;
}
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("noexcept(y.bar())"),
"ours's rename inside the noexcept clause must survive: {text}"
);
assert!(
text.contains("int a = 99"),
"theirs's body edit must survive: {text}"
);
let f_count = text.matches("void f(S ").count();
assert_eq!(
f_count, 1,
"f must appear exactly once across the rename + body edit: got {f_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"param rename inside noexcept clause + disjoint body edit must merge cleanly: {text}"
);
}
#[test]
fn cpp_template_method_refactor_inline_to_out_of_class_merges_cleanly() {
let base = "\
template<class T> class A {
void foo() {
int a = 0;
(void)a;
}
};
";
let ours = "\
template<class T> class A {
void foo();
};
template<class T> void A<T>::foo() {
int a = 0;
(void)a;
}
";
let theirs = "\
template<class T> class A {
void foo() {
int a = 99;
(void)a;
}
};
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("void A<T>::foo()"),
"ours's out-of-class signature must survive: {text}"
);
assert!(
text.contains("int a = 99"),
"theirs's body edit must survive: {text}"
);
let foo_body_count = text.matches("int a = 99").count();
assert_eq!(
foo_body_count, 1,
"foo body must appear exactly once after the refactor: got {foo_body_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"inline-to-out-of-class refactor + disjoint body edit must merge cleanly: {text}"
);
}
#[test]
fn cpp_explicit_specializations_keep_distinct_scopes_under_reorder() {
let base = "\
void A<int>::foo() {
int x = 0;
(void)x;
}
void A<float>::foo() {
int y = 0;
(void)y;
}
";
let ours = "\
void A<char>::foo() {
int z = 0;
(void)z;
}
void A<int>::foo() {
int x = 0;
(void)x;
}
void A<float>::foo() {
int y = 0;
(void)y;
int yy = y;
(void)yy;
}
";
let theirs = "\
void A<int>::foo() {
int x = 0;
(void)x;
int xx = x;
(void)xx;
}
void A<float>::foo() {
int y = 0;
(void)y;
}
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
fn body_for(text: &str, header: &str) -> String {
let start = text.find(header).expect("expected header in merged output");
let after = &text[start..];
let close = after.find("\n}\n").expect("expected close brace");
after[..close + 3].to_string()
}
let char_body = body_for(&text, "void A<char>::foo()");
let int_body = body_for(&text, "void A<int>::foo()");
let float_body = body_for(&text, "void A<float>::foo()");
assert!(
char_body.contains("int z = 0"),
"A<char>::foo must keep its own body: {char_body}"
);
assert!(
!char_body.contains("int xx = x"),
"theirs's edit on A<int>::foo must NOT leak into A<char>::foo: {char_body}"
);
assert!(
!char_body.contains("int yy = y"),
"ours's edit on A<float>::foo must NOT leak into A<char>::foo: {char_body}"
);
assert!(
int_body.contains("int x = 0"),
"A<int>::foo must keep base's body: {int_body}"
);
assert!(
int_body.contains("int xx = x"),
"theirs's edit on A<int>::foo must survive: {int_body}"
);
assert!(
!int_body.contains("int z = 0"),
"A<char>::foo body must NOT leak into A<int>::foo: {int_body}"
);
assert!(
float_body.contains("int y = 0"),
"A<float>::foo must keep base's body: {float_body}"
);
assert!(
float_body.contains("int yy = y"),
"ours's edit on A<float>::foo must survive: {float_body}"
);
let int_count = text.matches("A<int>::foo()").count();
let float_count = text.matches("A<float>::foo()").count();
let char_count = text.matches("A<char>::foo()").count();
assert_eq!(
int_count, 1,
"A<int>::foo must appear exactly once: got {int_count}: {text}"
);
assert_eq!(
float_count, 1,
"A<float>::foo must appear exactly once: got {float_count}: {text}"
);
assert_eq!(
char_count, 1,
"A<char>::foo must appear exactly once: got {char_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint edits on distinct specializations must merge cleanly: {text}"
);
}
#[test]
fn cpp_overloads_with_function_pointer_in_scope_template_arg_stay_distinct() {
let base = "\
void A<int(*)(double)>::foo(int x) {
int a = 0;
(void)a;
(void)x;
}
void A<int(*)(double)>::foo(char y) {
int b = 0;
(void)b;
(void)y;
}
";
let ours = "\
void A<int(*)(double)>::foo(short s) {
int c = 0;
(void)c;
(void)s;
}
void A<int(*)(double)>::foo(int x) {
int a = 0;
(void)a;
(void)x;
}
void A<int(*)(double)>::foo(char y) {
int b = 0;
(void)b;
(void)y;
int bb = b;
(void)bb;
}
";
let theirs = "\
void A<int(*)(double)>::foo(int x) {
int a = 0;
(void)a;
(void)x;
int aa = a;
(void)aa;
}
void A<int(*)(double)>::foo(char y) {
int b = 0;
(void)b;
(void)y;
}
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
fn body_for(text: &str, header: &str) -> String {
let start = text.find(header).expect("expected header in merged output");
let after = &text[start..];
let close = after.find("\n}\n").expect("expected close brace");
after[..close + 3].to_string()
}
let short_body = body_for(&text, "void A<int(*)(double)>::foo(short s)");
let int_body = body_for(&text, "void A<int(*)(double)>::foo(int x)");
let char_body = body_for(&text, "void A<int(*)(double)>::foo(char y)");
assert!(
short_body.contains("int c = 0"),
"ours's inserted foo(short) must keep its own body: {short_body}"
);
assert!(
!short_body.contains("int aa = a"),
"theirs's edit on foo(int) must NOT leak into foo(short): {short_body}"
);
assert!(
!short_body.contains("int bb = b"),
"ours's edit on foo(char) must NOT leak into foo(short): {short_body}"
);
assert!(
int_body.contains("int aa = a"),
"theirs's edit on foo(int) must survive: {int_body}"
);
assert!(
!int_body.contains("int bb = b"),
"ours's edit on foo(char) must NOT leak into foo(int): {int_body}"
);
assert!(
char_body.contains("int bb = b"),
"ours's edit on foo(char) must survive: {char_body}"
);
assert!(
!char_body.contains("int aa = a"),
"theirs's edit on foo(int) must NOT leak into foo(char): {char_body}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint overload edits must merge cleanly: {text}"
);
}
#[test]
fn cpp_partial_specializations_keep_distinct_scopes_under_reorder() {
let base = "\
template<class T> void A<T*>::foo() {
int x = 0;
(void)x;
}
template<class T> void A<T&>::foo() {
int y = 0;
(void)y;
}
";
let ours = "\
template<class T> void A<T**>::foo() {
int z = 0;
(void)z;
}
template<class T> void A<T*>::foo() {
int x = 0;
(void)x;
}
template<class T> void A<T&>::foo() {
int y = 0;
(void)y;
int yy = y;
(void)yy;
}
";
let theirs = "\
template<class T> void A<T*>::foo() {
int x = 0;
(void)x;
int xx = x;
(void)xx;
}
template<class T> void A<T&>::foo() {
int y = 0;
(void)y;
}
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
fn body_for(text: &str, header: &str) -> String {
let start = text
.find(header)
.unwrap_or_else(|| panic!("expected header {header:?} in merged output: {text}"));
let after = &text[start..];
let close = after.find("\n}\n").expect("expected close brace");
after[..close + 3].to_string()
}
let pp_body = body_for(&text, "void A<T**>::foo()");
let p_body = body_for(&text, "void A<T*>::foo()");
let r_body = body_for(&text, "void A<T&>::foo()");
assert!(
pp_body.contains("int z = 0"),
"A<T**>::foo must keep its own body: {pp_body}"
);
assert!(
!pp_body.contains("int xx = x"),
"theirs's edit on A<T*>::foo must NOT leak into A<T**>::foo: {pp_body}"
);
assert!(
!pp_body.contains("int yy = y"),
"ours's edit on A<T&>::foo must NOT leak into A<T**>::foo: {pp_body}"
);
assert!(
p_body.contains("int x = 0"),
"A<T*>::foo must keep base's body: {p_body}"
);
assert!(
p_body.contains("int xx = x"),
"theirs's edit on A<T*>::foo must survive: {p_body}"
);
assert!(
!p_body.contains("int z = 0"),
"A<T**>::foo body must NOT leak into A<T*>::foo: {p_body}"
);
assert!(
r_body.contains("int y = 0"),
"A<T&>::foo must keep base's body: {r_body}"
);
assert!(
r_body.contains("int yy = y"),
"ours's edit on A<T&>::foo must survive: {r_body}"
);
let pp_count = text.matches("A<T**>::foo()").count();
let p_count = text.matches("A<T*>::foo()").count();
let r_count = text.matches("A<T&>::foo()").count();
assert_eq!(
pp_count, 1,
"A<T**>::foo must appear exactly once: got {pp_count}: {text}"
);
assert_eq!(
p_count, 1,
"A<T*>::foo must appear exactly once: got {p_count}: {text}"
);
assert_eq!(
r_count, 1,
"A<T&>::foo must appear exactly once: got {r_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"disjoint edits on distinct partial specializations must merge cleanly: {text}"
);
}
#[test]
fn cpp_variadic_template_method_refactor_inline_to_out_of_class_merges_cleanly() {
let base = "\
template<class... Ts> class A {
void foo() {
int a = 0;
(void)a;
}
};
";
let ours = "\
template<class... Ts> class A {
void foo();
};
template<class... Ts> void A<Ts...>::foo() {
int a = 0;
(void)a;
}
";
let theirs = "\
template<class... Ts> class A {
void foo() {
int a = 77;
(void)a;
}
};
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("void A<Ts...>::foo()"),
"ours's out-of-class signature must survive: {text}"
);
assert!(
text.contains("int a = 77"),
"theirs's body edit must survive: {text}"
);
let foo_body_count = text.matches("int a = 77").count();
assert_eq!(
foo_body_count, 1,
"foo body must appear exactly once after the refactor: got {foo_body_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"variadic inline-to-out-of-class refactor + disjoint body edit must merge cleanly: {text}"
);
}
#[test]
fn cpp_template_template_param_method_refactor_inline_to_out_of_class_merges_cleanly() {
let base = "\
template<template<class> class Tmpl> class A {
void foo() {
int a = 0;
(void)a;
}
};
";
let ours = "\
template<template<class> class Tmpl> class A {
void foo();
};
template<template<class> class Tmpl> void A<Tmpl>::foo() {
int a = 0;
(void)a;
}
";
let theirs = "\
template<template<class> class Tmpl> class A {
void foo() {
int a = 55;
(void)a;
}
};
";
let outcome = merge_at(base, ours, theirs, "f.cpp");
let text = match outcome {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
MergeOutcome::Conflicts {
merged_bytes_with_markers,
..
} => String::from_utf8(merged_bytes_with_markers).unwrap(),
other => panic!("unexpected: {other:?}"),
};
assert!(
text.contains("void A<Tmpl>::foo()"),
"ours's out-of-class signature must survive: {text}"
);
assert!(
text.contains("int a = 55"),
"theirs's body edit must survive: {text}"
);
let foo_body_count = text.matches("int a = 55").count();
assert_eq!(
foo_body_count, 1,
"foo body must appear exactly once after the refactor: got {foo_body_count}: {text}"
);
assert!(
!text.contains("<<<<<<<"),
"template-template inline-to-out-of-class refactor + disjoint body edit must merge cleanly: {text}"
);
}
#[test]
fn rust_disjoint_use_additions_auto_combine() {
let base = "\
pub use crate::existing::Thing;
fn anchor() { 0 }
";
let ours = "\
pub use crate::aaa::Alpha;
pub use crate::existing::Thing;
fn anchor() { 0 }
";
let theirs = "\
pub use crate::bbb::Beta;
pub use crate::existing::Thing;
fn anchor() { 0 }
";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.contains("crate::aaa::Alpha"),
"ours re-export lost: {merged}"
);
assert!(
merged.contains("crate::bbb::Beta"),
"theirs re-export lost: {merged}"
);
assert!(
merged.contains("crate::existing::Thing"),
"base re-export lost: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"additive disjoint re-exports must merge cleanly: {merged}"
);
}
#[test]
fn rust_disjoint_use_additions_from_empty_base_combine() {
let base = "fn anchor() { 0 }\n";
let ours = "use std::collections::HashMap;\nfn anchor() { 0 }\n";
let theirs = "use std::fmt::Display;\nfn anchor() { 0 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("HashMap"), "ours use lost: {merged}");
assert!(merged.contains("Display"), "theirs use lost: {merged}");
assert!(
!merged.contains("<<<<<<<"),
"disjoint use additions must merge cleanly: {merged}"
);
}
#[test]
fn rust_same_path_divergent_use_addadd_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "pub use crate::foo::Bar;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let (_text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected a conflict on same-path divergence");
}
#[test]
fn rust_identical_use_addition_dedups_clean() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "pub use crate::shared::Thing;\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "pub use crate::shared::Thing;\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("crate::shared::Thing").count(),
1,
"identical re-export must appear exactly once: {merged}"
);
assert!(
merged.contains("fn alpha() { 10 }"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("fn beta() { 20 }"),
"theirs edit lost: {merged}"
);
}
#[test]
fn rust_grouped_vs_ungrouped_overlap_does_not_duplicate() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::{Bar, Baz};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let outcome = merge_rust(base, ours, theirs);
let (text, count) = assert_conflicts(outcome);
assert!(
count >= 1,
"overlapping grouped/ungrouped imports must conflict, not union: {text}"
);
}
#[test]
fn rust_identical_grouped_use_dedups_clean() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "pub use crate::foo::{Bar, Baz};\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "pub use crate::foo::{Bar, Baz};\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("crate::foo::{Bar, Baz}").count(),
1,
"identical grouped re-export must appear exactly once: {merged}"
);
assert!(
merged.contains("fn alpha() { 10 }"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("fn beta() { 20 }"),
"theirs edit lost: {merged}"
);
}
#[test]
fn rust_distinct_reexports_still_auto_combine() {
let base = "fn anchor() { 0 }\n";
let ours = "pub use crate::a::X;\nfn anchor() { 0 }\n";
let theirs = "pub use crate::b::Y;\nfn anchor() { 0 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.contains("crate::a::X"),
"ours re-export lost: {merged}"
);
assert!(
merged.contains("crate::b::Y"),
"theirs re-export lost: {merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"distinct re-exports must merge cleanly: {merged}"
);
}
#[test]
fn rust_glob_alias_unnormalizable_conflicts_not_misunion() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::*;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar as Renamed;\nfn anchor() { 0 }\n";
let outcome = merge_rust(base, ours, theirs);
let (text, count) = assert_conflicts(outcome);
assert!(
count >= 1,
"un-normalizable glob/alias adds must conflict, not mis-union: {text}"
);
}
#[test]
fn rust_grouped_vs_ungrouped_overlap_on_nonmin_leaf_does_not_duplicate() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::{Bar, Baz};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Baz;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"overlap on the non-minimum leaf must conflict, not union: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_three_leaf_group_overlap_on_nonmin_leaf_collides() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::m::{Ccc, Bbb, Aaa};\nfn anchor() { 0 }\n";
let theirs = "use crate::m::Ccc;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"overlap on the largest leaf must collide (conflict), not union: {text}"
);
}
#[test]
fn rust_single_element_group_vs_plain_same_visibility_conflicts() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "use crate::foo::{Baz};\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "use crate::foo::Baz;\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"same leaf spelled two ways is not byte-identical → must conflict, \
not partial-signal dedup: {text}"
);
}
#[test]
fn rust_same_leaf_divergent_cfg_attribute_conflicts_not_silent_drop() {
let base = "fn anchor() { 0 }\n";
let ours = "#[cfg(unix)]\nuse crate::foo::Bar;\nfn anchor() { 0 }\n";
let theirs = "#[cfg(windows)]\nuse crate::foo::Bar;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"same leaf with divergent #[cfg] must conflict, not silently drop a \
platform's import: {text}"
);
assert!(
text.contains("cfg(unix)") && text.contains("cfg(windows)"),
"both platform attributes must survive in the conflict region — \
neither side may be silently dropped: {text}"
);
}
#[test]
fn rust_same_leaf_divergent_alias_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::Bar as B;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar as C;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"same leaf with divergent alias must conflict, not dedup: {text}"
);
}
#[test]
fn rust_identical_attributed_use_addition_dedups_clean() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "#[cfg(unix)]\nuse crate::shared::Thing;\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "#[cfg(unix)]\nuse crate::shared::Thing;\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("crate::shared::Thing").count(),
1,
"byte-identical attributed re-export must dedup to one line: {merged}"
);
assert_eq!(
merged.matches("cfg(unix)").count(),
1,
"the shared attribute must appear exactly once: {merged}"
);
assert!(
merged.contains("fn alpha() { 10 }"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("fn beta() { 20 }"),
"theirs edit lost: {merged}"
);
}
#[test]
fn rust_single_element_group_vs_plain_divergent_visibility_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "pub use crate::foo::{Baz};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Baz;\nfn anchor() { 0 }\n";
let (_text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"same leaves but divergent visibility must conflict, not dedup"
);
}
#[test]
fn rust_empty_base_overlap_on_nonmin_leaf_conflicts() {
let base = "";
let ours = "use crate::n::{Yy, Zz};\n";
let theirs = "use crate::n::Zz;\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"empty-base overlap on non-min leaf must conflict, not duplicate: {text}"
);
}
#[test]
fn rust_use_base_widened_vs_separate_add_conflicts_not_duplicate() {
let base = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let ours = "use crate::foo::Bar;\nuse crate::foo::Baz;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::{Bar, Baz};\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"base-widened group vs separate-leaf add must conflict, not silently \
emit both (a duplicate `Baz` import): {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_base_widened_vs_separate_add_conflicts_mirror() {
let base = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let ours = "use crate::foo::{Bar, Baz};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nuse crate::foo::Baz;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"mirror of the base-widened case must also conflict: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_both_widen_base_differently_conflicts() {
let base = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let ours = "use crate::foo::{Bar, Baz};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::{Bar, Qux};\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"both sides widening the same base item differently must conflict: {text}"
);
}
#[test]
fn rust_use_disjoint_additions_still_combine_set_path() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::a::X;\nfn anchor() { 0 }\n";
let theirs = "use crate::b::Y;\nfn anchor() { 0 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(merged.contains("crate::a::X"), "ours add lost: {merged}");
assert!(merged.contains("crate::b::Y"), "theirs add lost: {merged}");
assert!(
!merged.contains("<<<<<<<"),
"disjoint additions must combine cleanly: {merged}"
);
}
#[test]
fn rust_use_identical_multiline_component_dedups_clean() {
let base = "use crate::foo::{Bar, Baz};\nfn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "use crate::foo::Bar;\nuse crate::foo::Baz;\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "use crate::foo::Bar;\nuse crate::foo::Baz;\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("crate::foo::Bar").count(),
1,
"Bar must appear exactly once after dedup: {merged}"
);
assert_eq!(
merged.matches("crate::foo::Baz").count(),
1,
"Baz must appear exactly once after dedup: {merged}"
);
assert!(
merged.contains("fn alpha() { 10 }"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("fn beta() { 20 }"),
"theirs edit lost: {merged}"
);
}
#[test]
fn rust_use_self_group_vs_plain_conflicts_not_duplicate() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::{self, Bar};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"self-group overlapping a plain import must conflict, not duplicate: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_nested_group_vs_plain_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::{bar::{Baz}};\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::bar::Baz;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"nested group overlapping a plain import must conflict: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_glob_vs_plain_overlap_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::*;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"glob overlapping a plain import must conflict, not mis-union: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_alias_vs_plain_same_leaf_conflicts() {
let base = "fn anchor() { 0 }\n";
let ours = "use crate::foo::Bar as B;\nfn anchor() { 0 }\n";
let theirs = "use crate::foo::Bar;\nfn anchor() { 0 }\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"alias overlapping the plain leaf it renames must conflict: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region, not a silent union: {text}"
);
}
#[test]
fn rust_use_identical_glob_both_sides_dedups_clean() {
let base = "fn alpha() { 1 }\nfn beta() { 2 }\n";
let ours = "use crate::foo::*;\nfn alpha() { 10 }\nfn beta() { 2 }\n";
let theirs = "use crate::foo::*;\nfn alpha() { 1 }\nfn beta() { 20 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert_eq!(
merged.matches("crate::foo::*").count(),
1,
"byte-identical glob must dedup to one line, not conflict: {merged}"
);
assert!(
merged.contains("fn alpha() { 10 }"),
"ours edit lost: {merged}"
);
assert!(
merged.contains("fn beta() { 20 }"),
"theirs edit lost: {merged}"
);
}
#[test]
fn rust_use_nested_glob_does_not_poison_disjoint_top_level_adds() {
let base = "mod m {\n use x::*;\n}\nfn anchor() { 0 }\n";
let ours = "use a::A;\nmod m {\n use x::*;\n}\nfn anchor() { 0 }\n";
let theirs = "use b::B;\nmod m {\n use x::*;\n}\nfn anchor() { 0 }\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.contains("use a::A;"),
"ours top-level add lost: {merged}"
);
assert!(
merged.contains("use b::B;"),
"theirs top-level add lost: {merged}"
);
assert_eq!(
merged.matches("use x::*;").count(),
1,
"unchanged nested glob must dedup to one copy: {merged}"
);
}
#[test]
fn rust_use_nested_glob_still_poisons_its_own_scope() {
let base = "mod m {\n use x::*;\n use crate::foo::Thing;\n}\n";
let ours = "mod m {\n use x::*;\n use crate::foo::ThingA;\n}\n";
let theirs = "mod m {\n use x::*;\n use crate::foo::ThingB;\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(
count >= 1,
"divergent use edits inside a glob-poisoned scope must conflict: {text}"
);
assert!(
text.contains("<<<<<<<"),
"expected a conflict region in the poisoned nested scope: {text}"
);
}
fn item_identities(
source: &str,
language: crate::parser::Language,
) -> std::collections::BTreeSet<(Vec<String>, String, String)> {
let parsed = crate::parser::ParsedFile::parse(source, language).expect("input must parse");
let segs = super::items::segment_file(&parsed);
let mut out = std::collections::BTreeSet::new();
super::items::visit_items(&segs.items, &mut |i| {
out.insert((
i.key.scope.clone(),
format!("{:?}", i.key.kind),
i.key.name.clone(),
));
});
out
}
fn assert_conformant(base: &str, ours: &str, theirs: &str) -> String {
assert_conformant_at(base, ours, theirs, "a.rs", crate::parser::Language::Rust)
}
fn assert_conformant_at(
base: &str,
ours: &str,
theirs: &str,
path: &str,
language: crate::parser::Language,
) -> String {
let out = assert_clean(merge_at(base, ours, theirs, path));
assert!(
crate::parser::ParsedFile::parse(&out, language).is_some(),
"clean merge produced an unparseable file:\n{out}"
);
let mut expected = item_identities(base, language);
expected.extend(item_identities(ours, language));
expected.extend(item_identities(theirs, language));
let got = item_identities(&out, language);
assert_eq!(
got, expected,
"item-set / nesting not conserved\n got: {got:?}\n want: {expected:?}\n output:\n{out}"
);
out
}
#[test]
fn conformance_484_structural_matrix() {
let cases: &[(&str, &str, &str)] = &[
(
"pub fn a() {}\n\n// MARK\n",
"pub fn a() {}\n\npub fn c() {}\n\n// MARK\n",
"pub fn a() {}\n\npub fn b() {}\n\n// MARK\n",
),
(
"pub fn a() {}\n",
"pub fn a() {}\n\npub fn c() {}\n",
"pub fn a() {}\n\npub fn b() {}\n",
),
(
"pub fn z() {}\n",
"pub fn c() {}\n\npub fn z() {}\n",
"pub fn b() {}\n\npub fn z() {}\n",
),
(
"pub fn a() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n",
"pub fn a() {}\n\npub fn c() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n",
"pub fn a() {}\n\npub fn b() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n",
),
(
"#[cfg(test)]\nmod tests {\n fn t() {}\n}\n",
"#[cfg(test)]\nmod tests {\n fn t() {}\n}\n\npub fn ours_after() {}\n",
"#[cfg(test)]\nmod tests {\n fn t() {}\n}\n\npub fn theirs_after() {}\n",
),
(
"mod tests {\n fn t() {}\n}\n",
"mod tests {\n fn t() {}\n fn ours_t() {}\n}\n",
"mod tests {\n fn t() {}\n fn theirs_t() {}\n}\n",
),
(
"pub mod prelude {\n pub use crate::a;\n}\n",
"pub mod prelude {\n pub use crate::a;\n pub use crate::b;\n}\n\npub fn ours_fn() {}\n",
"pub mod prelude {\n pub use crate::a;\n pub use crate::c;\n}\n\npub fn theirs_fn() {}\n",
),
(
"pub mod prelude {\n pub use crate::a;\n}\n",
"pub fn ours_fn() {}\n\npub mod prelude {\n pub use crate::a;\n pub use crate::b;\n}\n",
"pub fn theirs_fn() {}\n\npub mod prelude {\n pub use crate::a;\n pub use crate::c;\n}\n",
),
(
"pub mod outer {\n pub mod inner {\n pub use crate::a;\n }\n}\n",
"pub mod outer {\n pub mod inner {\n pub use crate::a;\n pub use crate::b;\n }\n}\n\npub fn ours_fn() {}\n",
"pub mod outer {\n pub mod inner {\n pub use crate::a;\n pub use crate::c;\n }\n}\n\npub fn theirs_fn() {}\n",
),
(
"struct S;\nimpl S {\n fn base(&self) {}\n}\n",
"struct S;\nimpl S {\n fn base(&self) {}\n fn ours_m(&self) {}\n}\n\nfn ours_top() {}\n",
"struct S;\nimpl S {\n fn base(&self) {}\n fn theirs_m(&self) {}\n}\n\nfn theirs_top() {}\n",
),
];
for (base, ours, theirs) in cases {
assert_conformant(base, ours, theirs);
}
}
#[test]
fn repro_484_bug1_trailing_postamble_not_duplicated() {
let base = "pub fn a() {}\n\n// MARK\n";
let ours = "pub fn a() {}\n\npub fn c() {}\n\n// MARK\n";
let theirs = "pub fn a() {}\n\npub fn b() {}\n\n// MARK\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("// MARK").count(),
1,
"marker duplicated:\n{merged}"
);
assert!(
merged.contains("pub fn b() {}"),
"theirs add lost:\n{merged}"
);
assert!(merged.contains("pub fn c() {}"), "ours add lost:\n{merged}");
let mark = merged.find("// MARK").unwrap();
assert!(
merged.find("pub fn b() {}").unwrap() < mark,
"b after marker:\n{merged}"
);
assert!(
merged.find("pub fn c() {}").unwrap() < mark,
"c after marker:\n{merged}"
);
}
#[test]
fn repro_484_bug2_trailing_module_not_nested_or_duplicated() {
let base = "pub fn a() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n";
let ours = "pub fn a() {}\n\npub fn c() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n";
let theirs = "pub fn a() {}\n\npub fn b() {}\n\n#[cfg(test)]\nmod tests {\n fn t() {}\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("mod tests").count(),
1,
"module duplicated:\n{merged}"
);
let m = merged.find("mod tests").unwrap();
assert!(
merged.find("pub fn b() {}").unwrap() < m,
"b nested/after module:\n{merged}"
);
assert!(
merged.find("pub fn c() {}").unwrap() < m,
"c nested/after module:\n{merged}"
);
assert!(merged.contains("fn t() {}"), "module body lost:\n{merged}");
}
#[test]
fn repro_484_bug3_nested_pub_use_stays_inside_module() {
let base = "pub mod prelude {\n pub use crate::a;\n}\n";
let ours = "pub mod prelude {\n pub use crate::a;\n pub use crate::b;\n}\n\npub fn ours_fn() {}\n";
let theirs = "pub mod prelude {\n pub use crate::a;\n pub use crate::c;\n}\n\npub fn theirs_fn() {}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("pub mod prelude").count(),
1,
"module duplicated:\n{merged}"
);
let close = merged.find("}").unwrap();
assert!(
merged.find("pub use crate::b").unwrap() < close,
"b escaped module:\n{merged}"
);
assert!(
merged.find("pub use crate::c").unwrap() < close,
"c escaped module:\n{merged}"
);
assert!(
merged.find("pub fn ours_fn").unwrap() > close,
"ours_fn inside module:\n{merged}"
);
assert!(
merged.find("pub fn theirs_fn").unwrap() > close,
"theirs_fn inside module:\n{merged}"
);
}
#[test]
fn repro_484_reopened_rust_impl_keeps_top_level_between_blocks() {
let base = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n fn a2(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) {}\n fn b2(&self) {}\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert!(
merged.contains("fn a2(&self) {}"),
"ours add lost:\n{merged}"
);
assert!(
merged.contains("fn b2(&self) {}"),
"theirs add lost:\n{merged}"
);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"reopened impls collapsed:\n{merged}"
);
let first = merged.find("impl Foo").unwrap();
let top = merged.find("fn top_level()").unwrap();
let last = merged.rfind("impl Foo").unwrap();
assert!(
first < top && top < last,
"top-level fn must sit between impls:\n{merged}"
);
assert!(
merged.find("fn a2(&self) {}").unwrap() < top,
"a2 escaped first impl:\n{merged}"
);
assert!(
merged.find("fn b2(&self) {}").unwrap() > top,
"b2 escaped second impl:\n{merged}"
);
}
#[test]
fn repro_484_reopened_rust_impl_add_first_modify_second_keeps_order() {
let base = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n fn a2(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nfn top_level() {}\n\nimpl Foo {\n fn b(&self) { let _ = 1; }\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert!(
merged.contains("fn a2(&self) {}"),
"ours add lost:\n{merged}"
);
assert!(
merged.contains("fn b(&self) { let _ = 1; }"),
"theirs edit lost:\n{merged}"
);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"reopened impls collapsed:\n{merged}"
);
let first = merged.find("impl Foo").unwrap();
let top = merged.find("fn top_level()").unwrap();
let last = merged.rfind("impl Foo").unwrap();
assert!(
first < top && top < last,
"top-level fn must sit between impls:\n{merged}"
);
}
fn impl_reopen_triple(sep: &str) -> (String, String, String) {
let base = format!(
"struct Foo;\n\nimpl Foo {{\n fn a(&self) {{}}\n}}{sep}impl Foo {{\n fn b(&self) {{}}\n}}\n"
);
let ours = format!(
"struct Foo;\n\nimpl Foo {{\n fn a(&self) {{}}\n fn a2(&self) {{}}\n}}{sep}impl Foo {{\n fn b(&self) {{}}\n}}\n"
);
let theirs = format!(
"struct Foo;\n\nimpl Foo {{\n fn a(&self) {{}}\n}}{sep}impl Foo {{\n fn b(&self) {{}}\n fn b2(&self) {{}}\n}}\n"
);
(base, ours, theirs)
}
#[test]
fn conformance_484_reopened_rust_impl_separator_matrix() {
let cases: &[(&str, &str)] = &[
("back-to-back", "\n"),
("whitespace-separated", "\n\n"),
("comment-separated", "\n// section\n"),
("item-separated", "\n\nfn top_level() {}\n\n"),
];
for (label, sep) in cases {
let (base, ours, theirs) = impl_reopen_triple(sep);
let merged = assert_conformant(&base, &ours, &theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"[{label}] reopened impls collapsed:\n{merged}"
);
assert!(
merged.contains("fn a2(&self) {}"),
"[{label}] ours add lost:\n{merged}"
);
assert!(
merged.contains("fn b2(&self) {}"),
"[{label}] theirs add lost:\n{merged}"
);
let second_impl = merged.rfind("impl Foo").unwrap();
assert!(
merged.find("fn a2(&self) {}").unwrap() < second_impl,
"[{label}] a2 escaped first block:\n{merged}"
);
assert!(
merged.find("fn b2(&self) {}").unwrap() > second_impl,
"[{label}] b2 escaped second block:\n{merged}"
);
if sep.contains("// section") {
assert_eq!(
merged.matches("// section").count(),
1,
"[{label}] comment separator dropped/duplicated:\n{merged}"
);
}
if sep.contains("fn top_level") {
let first_impl = merged.find("impl Foo").unwrap();
let top = merged.find("fn top_level()").unwrap();
assert!(
first_impl < top && top < second_impl,
"[{label}] top-level item must stay between blocks:\n{merged}"
);
}
}
}
#[test]
fn repro_484_r3_prepend_new_impl_before_base_block_no_collapse() {
let base = "struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours =
"struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn b(&self) { let _ = 1; }\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"prepended impl collapsed:\n{merged}"
);
assert!(
merged.contains("fn a(&self) {}"),
"ours add lost:\n{merged}"
);
assert!(
merged.contains("fn b(&self) { let _ = 1; }"),
"theirs edit lost:\n{merged}"
);
assert!(
merged.find("fn a(&self) {}").unwrap() < merged.find("fn b(&self)").unwrap(),
"prepended block must precede base block:\n{merged}"
);
}
#[test]
fn conformance_484_r3_cross_side_container_alignment_matrix() {
let cases: &[(&str, &str, &str, &str)] = &[
(
"prepend",
"struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n",
"struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n",
"struct Foo;\n\nimpl Foo {\n fn b(&self) { let _ = 1; }\n}\n",
),
(
"append",
"struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n",
"struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n\nimpl Foo {\n fn a(&self) {}\n}\n",
"struct Foo;\n\nimpl Foo {\n fn b(&self) { let _ = 1; }\n}\n",
),
];
for (label, base, ours, theirs) in cases {
let merged = assert_conformant(base, ours, theirs);
for needle in ["fn a", "fn b"] {
assert!(
merged.contains(needle),
"[{label}] {needle} lost:\n{merged}"
);
}
assert!(
crate::parser::ParsedFile::parse(&merged, crate::parser::Language::Rust).is_some(),
"[{label}] unparseable merge:\n{merged}"
);
}
}
#[test]
fn reopened_impl_both_add_new_block_no_base_conflicts() {
let base = "struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours =
"struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let theirs =
"struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n\nimpl Foo {\n fn c(&self) {}\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected ≥1 conflict, got {count}: {text}");
assert!(
text.contains("<<<<<<<") && text.contains("=======") && text.contains(">>>>>>>"),
"expected canonical conflict markers: {text}"
);
assert!(
text.contains("fn a(&self)") && text.contains("fn c(&self)"),
"both new blocks' bodies must survive inside the conflict: {text}"
);
assert!(
text.contains("fn b(&self)"),
"base-anchored block lost: {text}"
);
}
#[test]
fn repro_484_r3_reordered_container_no_silent_collapse() {
let base =
"struct Foo;\n\nimpl Foo {\n fn a(&self) {}\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours =
"struct Foo;\n\nimpl Foo {\n fn b(&self) {}\n}\n\nimpl Foo {\n fn a(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn a(&self) { let _ = 1; }\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let text = match merge_rust(base, ours, theirs) {
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => {
assert!(conflict_count >= 1);
String::from_utf8(merged_bytes_with_markers).unwrap()
}
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
assert!(
crate::parser::ParsedFile::parse(&text, crate::parser::Language::Rust).is_some(),
"clean merge is unparseable (silent corruption):\n{text}"
);
text
}
other => panic!("unexpected outcome: {other:?}"),
};
assert!(text.contains("fn a(&self)"), "fn a lost:\n{text}");
assert!(text.contains("fn b(&self)"), "fn b lost:\n{text}");
}
#[test]
fn repro_484_r3_ambiguous_block_merge_no_silent_collapse() {
let base = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 0 }\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 9 }\n fn b(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 1 }\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
match merge_rust(base, ours, theirs) {
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => {
let text = String::from_utf8(merged_bytes_with_markers).unwrap();
assert!(conflict_count >= 1, "expected a real conflict");
assert!(text.contains("fn a(&self)"), "fn a lost:\n{text}");
assert!(text.contains("fn b(&self)"), "fn b lost:\n{text}");
}
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
assert!(
crate::parser::ParsedFile::parse(&text, crate::parser::Language::Rust).is_some(),
"clean merge is unparseable:\n{text}"
);
assert!(
text.contains("fn a(&self)") && text.contains("fn b(&self)"),
"clean merge dropped a block:\n{text}"
);
}
other => panic!("unexpected outcome: {other:?}"),
}
}
#[test]
fn repro_484_r3_ambiguous_block_split_no_silent_collapse() {
let base = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 0 }\n fn b(&self) {}\n}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 9 }\n}\n\nimpl Foo {\n fn b(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn a(&self) -> u32 { 1 }\n fn b(&self) {}\n}\n";
let text = match merge_rust(base, ours, theirs) {
MergeOutcome::Conflicts {
merged_bytes_with_markers,
conflict_count,
} => {
assert!(conflict_count >= 1);
String::from_utf8(merged_bytes_with_markers).unwrap()
}
MergeOutcome::Clean(bytes) => {
let text = String::from_utf8(bytes).unwrap();
assert!(
crate::parser::ParsedFile::parse(&text, crate::parser::Language::Rust).is_some(),
"clean merge unparseable:\n{text}"
);
text
}
other => panic!("unexpected outcome: {other:?}"),
};
assert!(text.contains("fn a(&self)"), "fn a lost:\n{text}");
assert!(text.contains("fn b(&self)"), "fn b lost:\n{text}");
}
#[test]
fn repro_490_r1_two_empty_impl_blocks_one_side_adds_method_clean() {
let base = "struct Foo;\n\nimpl Foo {}\n\nimpl Foo {}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn m(&self) {}\n}\n\nimpl Foo {}\n";
let theirs = base;
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"empty blocks dropped/duplicated:\n{merged}"
);
assert!(
merged.contains("fn m(&self) {}"),
"ours add lost:\n{merged}"
);
let first = merged.find("impl Foo").unwrap();
let second = merged.rfind("impl Foo").unwrap();
let m = merged.find("fn m(&self) {}").unwrap();
assert!(
first < m && m < second,
"method escaped the first block:\n{merged}"
);
}
#[test]
fn repro_490_r1_two_empty_impl_blocks_each_side_edits_different_block_clean() {
let base = "struct Foo;\n\nimpl Foo {}\n\nimpl Foo {}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn ours_m(&self) {}\n}\n\nimpl Foo {}\n";
let theirs = "struct Foo;\n\nimpl Foo {}\n\nimpl Foo {\n fn theirs_m(&self) {}\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"empty blocks dropped/duplicated:\n{merged}"
);
assert!(
merged.contains("fn ours_m(&self) {}"),
"ours edit lost:\n{merged}"
);
assert!(
merged.contains("fn theirs_m(&self) {}"),
"theirs edit lost:\n{merged}"
);
assert!(
merged.find("fn ours_m(&self) {}").unwrap() < merged.find("fn theirs_m(&self) {}").unwrap(),
"edits landed in the wrong blocks:\n{merged}"
);
}
#[test]
fn repro_490_r1_fully_replaced_impl_children_align_to_base_slot_no_conflict() {
let base = "struct Foo;\n\nimpl Foo {\n fn x(&self) {}\n}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn y(&self) {}\n}\n";
let theirs = "struct Foo;\n\nimpl Foo {\n fn x(&self) {}\n fn z(&self) {}\n}\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
crate::parser::ParsedFile::parse(&merged, crate::parser::Language::Rust).is_some(),
"unparseable merge:\n{merged}"
);
assert_eq!(
merged.matches("impl Foo").count(),
1,
"block duplicated:\n{merged}"
);
assert!(
merged.contains("fn y(&self) {}"),
"ours replacement lost:\n{merged}"
);
assert!(
merged.contains("fn z(&self) {}"),
"theirs add lost:\n{merged}"
);
assert!(
!merged.contains("fn x(&self) {}"),
"ours deletion of fn x dropped:\n{merged}"
);
}
#[test]
fn repro_490_r1_fully_replaced_mod_children_align_to_base_slot_no_conflict() {
let base = "mod m {\n fn x() {}\n}\n";
let ours = "mod m {\n fn y() {}\n}\n";
let theirs = "mod m {\n fn x() {}\n fn z() {}\n}\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
crate::parser::ParsedFile::parse(&merged, crate::parser::Language::Rust).is_some(),
"unparseable merge:\n{merged}"
);
assert_eq!(
merged.matches("mod m").count(),
1,
"mod duplicated:\n{merged}"
);
assert!(
merged.contains("fn y() {}"),
"ours replacement lost:\n{merged}"
);
assert!(merged.contains("fn z() {}"), "theirs add lost:\n{merged}");
assert!(
!merged.contains("fn x() {}"),
"ours deletion of fn x dropped:\n{merged}"
);
}
#[cfg(feature = "lang-cpp")]
#[test]
fn repro_490_r1_two_empty_cpp_namespaces_one_side_adds_clean() {
let base = "namespace N {}\n\nnamespace N {}\n";
let ours = "namespace N {\nvoid m() {}\n}\n\nnamespace N {}\n";
let theirs = base;
let merged = assert_conformant_at(base, ours, theirs, "f.cpp", crate::parser::Language::Cpp);
assert_eq!(
merged.matches("namespace N").count(),
2,
"empty namespaces dropped/duplicated:\n{merged}"
);
assert!(merged.contains("void m() {}"), "ours add lost:\n{merged}");
let first = merged.find("namespace N").unwrap();
let second = merged.rfind("namespace N").unwrap();
let m = merged.find("void m() {}").unwrap();
assert!(
first < m && m < second,
"function escaped the first namespace:\n{merged}"
);
}
#[test]
fn repro_490_r1_modify_vs_delete_empty_container_conflicts() {
let base = "struct Foo;\n\nimpl Foo {}\n";
let ours = "struct Foo;\n\nimpl Foo {\n fn m(&self) {}\n}\n";
let theirs = "struct Foo;\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected a modify/delete conflict, got {count}");
assert!(
text.contains("fn m(&self) {}"),
"ours modification lost from conflict:\n{text}"
);
assert!(
text.contains("<<<<<<<") && text.contains(">>>>>>>"),
"missing conflict markers:\n{text}"
);
}
#[test]
fn repro_490_r2_delete_earlier_edit_later_zero_overlap_separator_not_moved() {
let base = "impl Foo {}\n// sep\nimpl Foo {}\n";
let ours = "// sep\nimpl Foo {\n fn m(&self) {}\n}\n";
let theirs = "impl Foo {}\n// edited sep\nimpl Foo {}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
1,
"block dropped/duplicated:\n{merged}"
);
assert!(
merged.contains("fn m(&self) {}"),
"ours method lost:\n{merged}"
);
assert!(
merged.contains("// edited sep"),
"theirs separator edit lost:\n{merged}"
);
assert_eq!(
merged.matches("sep").count(),
1,
"separator moved/duplicated:\n{merged}"
);
}
#[test]
fn repro_490_r2_prepend_zero_overlap_keeps_survivor_base_slot() {
let base = "// orig\nimpl Foo {}\n";
let ours = "impl Foo {}\n\n// orig\nimpl Foo {}\n";
let theirs = "// orig\nimpl Foo {\n fn m(&self) {}\n}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"block dropped/duplicated:\n{merged}"
);
assert!(
merged.contains("fn m(&self) {}"),
"theirs method lost:\n{merged}"
);
assert_eq!(
merged.matches("// orig").count(),
1,
"comment moved/duplicated:\n{merged}"
);
assert!(
merged.find("// orig").unwrap() < merged.find("fn m(&self) {}").unwrap(),
"method escaped the original block:\n{merged}"
);
}
#[test]
fn repro_490_r2_reorder_zero_overlap_blocks_keep_identity_by_header() {
let base = "// a\nimpl Foo {}\n\n// b\nimpl Foo {}\n";
let ours = "// b\nimpl Foo {}\n\n// a\nimpl Foo {}\n";
let theirs = "// a\nimpl Foo {\n fn am(&self) {}\n}\n\n// b\nimpl Foo {}\n";
let merged = assert_conformant(base, ours, theirs);
assert_eq!(
merged.matches("impl Foo").count(),
2,
"block dropped/duplicated:\n{merged}"
);
assert!(
merged.contains("fn am(&self) {}"),
"theirs method lost:\n{merged}"
);
assert!(
merged.find("// a").unwrap() < merged.find("fn am(&self) {}").unwrap()
&& merged.find("fn am(&self) {}").unwrap() < merged.find("// b").unwrap(),
"method bound to the wrong block:\n{merged}"
);
}
#[test]
fn floor_490_conserves_inputs_rejects_unparseable_output() {
use crate::parser::{Language, ParsedFile};
let base = ParsedFile::parse("fn a() {}\n", Language::Rust).unwrap();
let ours = ParsedFile::parse("fn a() {}\n\nfn b() {}\n", Language::Rust).unwrap();
let theirs = ParsedFile::parse("fn a() {}\n", Language::Rust).unwrap();
let corrupt = b"fn a() { fn b() {\n";
assert!(
!super::conserves_inputs(corrupt, Language::Rust, &base, &ours, &theirs),
"floor must reject an unparseable clean output"
);
}
#[test]
fn floor_490_conserves_inputs_rejects_invented_or_misnested_item() {
use crate::parser::{Language, ParsedFile};
let src = "impl Foo {\n fn m(&self) {}\n}\n";
let base = ParsedFile::parse(src, Language::Rust).unwrap();
let ours = ParsedFile::parse(src, Language::Rust).unwrap();
let theirs = ParsedFile::parse(src, Language::Rust).unwrap();
let misnested = b"fn m(&self) {}\n\nimpl Foo {}\n";
assert!(
!super::conserves_inputs(misnested, Language::Rust, &base, &ours, &theirs),
"floor must reject an item escaped to a scope no input had"
);
}
#[test]
fn floor_490_conserves_inputs_allows_legitimate_deletion() {
use crate::parser::{Language, ParsedFile};
let base = ParsedFile::parse("fn a() {}\n\nfn b() {}\n", Language::Rust).unwrap();
let ours = ParsedFile::parse("fn a() {}\n", Language::Rust).unwrap();
let theirs = ParsedFile::parse("fn a() {}\n\nfn b() {}\n", Language::Rust).unwrap();
let deleted = b"fn a() {}\n";
assert!(
super::conserves_inputs(deleted, Language::Rust, &base, &ours, &theirs),
"floor must accept a clean deletion"
);
}
#[test]
fn floor_490_conserves_inputs_allows_within_line_edit_recombination() {
use crate::parser::{Language, ParsedFile};
let base = ParsedFile::parse("fn a() { let x = 1; }\n", Language::Rust).unwrap();
let ours = ParsedFile::parse("fn a() { let x = 2; }\n", Language::Rust).unwrap();
let theirs = ParsedFile::parse("fn a() { let y = 1; }\n", Language::Rust).unwrap();
let edited = b"fn a() { let x = 2; let y = 1; }\n";
assert!(
super::conserves_inputs(edited, Language::Rust, &base, &ours, &theirs),
"floor must accept an edit recombination"
);
}
#[test]
fn repro_490_r3_python_class_to_decorated_mixed_kind_no_panic_clean() {
let base = "\
class C:
def m(self):
return 1
";
let ours = "\
@deco
class C:
def m(self):
return 1
";
let theirs = "\
class C:
def m(self):
return 2
";
let merged = match merge_at(base, ours, theirs, "f.py") {
MergeOutcome::Clean(b) => String::from_utf8(b).unwrap(),
other => panic!("expected Clean, got {other:?}"),
};
assert!(merged.contains("@deco"), "ours decorator lost:\n{merged}");
assert!(
merged.contains("return 2"),
"theirs body edit lost:\n{merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"unexpected conflict markers:\n{merged}"
);
}
#[test]
fn repro_490_r3_python_class_to_decorated_mixed_kind_overlap_conflicts() {
let base = "\
class C:
def m(self):
return 1
";
let ours = "\
@deco
class C:
def m(self):
return 10
";
let theirs = "\
class C:
def m(self):
return 20
";
let (text, count) = assert_conflicts(merge_at(base, ours, theirs, "f.py"));
assert!(count >= 1, "expected a conflict, got {count}");
assert!(
text.contains("<<<<<<<") && text.contains(">>>>>>>"),
"missing conflict markers:\n{text}"
);
assert!(
text.contains("return 10"),
"ours side lost from conflict:\n{text}"
);
assert!(
text.contains("return 20"),
"theirs side lost from conflict:\n{text}"
);
}
#[test]
fn repro_490_r3_rust_mod_block_to_decl_mixed_kind_no_panic() {
let base = "mod foo {\n fn a() {}\n}\n";
let ours = "mod foo;\n";
let theirs = "mod foo {\n fn a() {\n let x = 1;\n }\n}\n";
let (text, count) = assert_conflicts(merge_rust(base, ours, theirs));
assert!(count >= 1, "expected a conflict, got {count}");
assert!(
text.contains("<<<<<<<") && text.contains(">>>>>>>"),
"missing conflict markers:\n{text}"
);
assert!(
text.contains("mod foo;"),
"ours leaf form lost from conflict:\n{text}"
);
assert!(
text.contains("let x = 1;"),
"theirs edit lost from conflict:\n{text}"
);
}
#[test]
fn repro_490_r3_rust_mod_decl_to_block_mixed_kind_disjoint_clean() {
let base = "mod foo;\n";
let ours = "mod foo {\n fn a() {}\n}\n";
let theirs = "mod foo;\n";
let merged = assert_clean(merge_rust(base, ours, theirs));
assert!(
merged.contains("fn a() {}"),
"ours expansion lost:\n{merged}"
);
assert!(
!merged.contains("<<<<<<<"),
"unexpected conflict markers:\n{merged}"
);
}
#[cfg(feature = "lang-cpp")]
fn namespace_reopen_triple(sep: &str) -> (String, String, String) {
let base = format!("namespace N {{\nvoid a() {{}}\n}}{sep}namespace N {{\nvoid b() {{}}\n}}\n");
let ours = format!(
"namespace N {{\nvoid a() {{}}\nvoid a2() {{}}\n}}{sep}namespace N {{\nvoid b() {{}}\n}}\n"
);
let theirs = format!(
"namespace N {{\nvoid a() {{}}\n}}{sep}namespace N {{\nvoid b() {{}}\nvoid b2() {{}}\n}}\n"
);
(base, ours, theirs)
}
#[cfg(feature = "lang-cpp")]
#[test]
fn conformance_484_reopened_cpp_namespace_separator_matrix() {
let cases: &[(&str, &str)] = &[
("back-to-back", "\n"),
("whitespace-separated", "\n\n"),
("comment-separated", "\n// section\n"),
("item-separated", "\n\nint top_level() { return 0; }\n\n"),
];
for (label, sep) in cases {
let (base, ours, theirs) = namespace_reopen_triple(sep);
let merged =
assert_conformant_at(&base, &ours, &theirs, "f.cpp", crate::parser::Language::Cpp);
assert_eq!(
merged.matches("namespace N").count(),
2,
"[{label}] reopened namespaces collapsed:\n{merged}"
);
assert!(
merged.contains("void a2() {}"),
"[{label}] ours add lost:\n{merged}"
);
assert!(
merged.contains("void b2() {}"),
"[{label}] theirs add lost:\n{merged}"
);
let second_ns = merged.rfind("namespace N").unwrap();
assert!(
merged.find("void a2() {}").unwrap() < second_ns,
"[{label}] a2 escaped first block:\n{merged}"
);
assert!(
merged.find("void b2() {}").unwrap() > second_ns,
"[{label}] b2 escaped second block:\n{merged}"
);
if sep.contains("// section") {
assert_eq!(
merged.matches("// section").count(),
1,
"[{label}] comment separator dropped/duplicated:\n{merged}"
);
}
if sep.contains("top_level") {
let first_ns = merged.find("namespace N").unwrap();
let top = merged.find("int top_level()").unwrap();
assert!(
first_ns < top && top < second_ns,
"[{label}] top-level item must stay between blocks:\n{merged}"
);
}
}
}
#[cfg(feature = "lang-cpp")]
#[test]
fn conformance_484_r3_cross_side_cpp_namespace_prepend_no_collapse() {
let base = "namespace N {\nvoid b() {}\n}\n";
let ours = "namespace N {\nvoid a() {}\n}\n\nnamespace N {\nvoid b() {}\n}\n";
let theirs = "namespace N {\nvoid b() { int x = 1; }\n}\n";
let merged = assert_conformant_at(base, ours, theirs, "f.cpp", crate::parser::Language::Cpp);
assert_eq!(
merged.matches("namespace N").count(),
2,
"prepended namespace collapsed into base block:\n{merged}"
);
assert!(merged.contains("void a() {}"), "ours add lost:\n{merged}");
assert!(
merged.contains("void b() { int x = 1; }"),
"theirs edit lost:\n{merged}"
);
assert!(
merged.find("void a()").unwrap() < merged.find("void b()").unwrap(),
"prepended block must precede the base block:\n{merged}"
);
}