use std::collections::HashMap;
use std::sync::Mutex;
use semantic_diff::app::{App, Message};
use semantic_diff::config::Config;
use semantic_diff::diff;
use semantic_diff::grouper::{
GroupedChange, GroupingStatus, SemanticGroup,
compute_all_file_hashes, compute_diff_delta, compute_file_hash,
incremental_hunk_summaries, merge_groups, normalize_hunk_indices,
remove_files_from_groups,
};
static PATH_MUTEX: Mutex<()> = Mutex::new(());
const DIFF_V1: &str = "\
diff --git a/src/auth.rs b/src/auth.rs
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -10,6 +10,8 @@ impl Auth {
fn login(&self) {
+ self.validate();
+ self.session_start();
}
@@ -30,3 +32,4 @@ impl Auth {
fn logout(&self) {
+ self.cleanup();
}
diff --git a/src/middleware.rs b/src/middleware.rs
--- a/src/middleware.rs
+++ b/src/middleware.rs
@@ -5,6 +5,7 @@ fn apply_middleware() {
setup();
+ auth_check();
}
";
const DIFF_V2: &str = "\
diff --git a/src/auth.rs b/src/auth.rs
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -10,6 +10,8 @@ impl Auth {
fn login(&self) {
+ self.validate();
+ self.session_start();
}
@@ -30,3 +32,4 @@ impl Auth {
fn logout(&self) {
+ self.cleanup();
}
diff --git a/src/middleware.rs b/src/middleware.rs
--- a/src/middleware.rs
+++ b/src/middleware.rs
@@ -5,6 +5,7 @@ fn apply_middleware() {
setup();
+ auth_check();
}
diff --git a/src/router.rs b/src/router.rs
--- /dev/null
+++ b/src/router.rs
@@ -0,0 +1,5 @@
+fn setup_routes() {
+ route(\"/login\", auth_handler);
+ route(\"/api\", api_handler);
+ route(\"/health\", health_handler);
+}
";
const DIFF_V3: &str = "\
diff --git a/src/auth.rs b/src/auth.rs
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -10,6 +10,9 @@ impl Auth {
fn login(&self) {
+ self.validate();
+ self.session_start();
+ self.audit_log();
}
@@ -30,3 +33,4 @@ impl Auth {
fn logout(&self) {
+ self.cleanup();
}
diff --git a/src/middleware.rs b/src/middleware.rs
--- a/src/middleware.rs
+++ b/src/middleware.rs
@@ -5,6 +5,7 @@ fn apply_middleware() {
setup();
+ auth_check();
}
";
const DIFF_V4: &str = "\
diff --git a/src/middleware.rs b/src/middleware.rs
--- a/src/middleware.rs
+++ b/src/middleware.rs
@@ -5,6 +5,7 @@ fn apply_middleware() {
setup();
+ auth_check();
}
";
fn make_group(label: &str, file: &str, hunks: Vec<usize>) -> SemanticGroup {
SemanticGroup::new(
label.to_string(),
String::new(),
vec![GroupedChange {
file: file.to_string(),
hunks,
}],
)
}
fn make_group_multi(label: &str, changes: Vec<(&str, Vec<usize>)>) -> SemanticGroup {
SemanticGroup::new(
label.to_string(),
String::new(),
changes
.into_iter()
.map(|(f, h)| GroupedChange {
file: f.to_string(),
hunks: h,
})
.collect(),
)
}
#[test]
fn test_file_hash_deterministic() {
let data = diff::parse(DIFF_V1);
let file = &data.files[0];
let h1 = compute_file_hash(file);
let h2 = compute_file_hash(file);
assert_eq!(h1, h2, "hash must be deterministic for the same file");
}
#[test]
fn test_file_hash_changes_with_content() {
let data_v1 = diff::parse(DIFF_V1);
let data_v3 = diff::parse(DIFF_V3);
let hash_v1 = compute_file_hash(&data_v1.files[0]);
let hash_v3 = compute_file_hash(&data_v3.files[0]);
assert_ne!(
hash_v1, hash_v3,
"hash must differ when file content changes"
);
}
#[test]
fn test_all_file_hashes_keys() {
let data = diff::parse(DIFF_V1);
let hashes = compute_all_file_hashes(&data);
assert!(
hashes.contains_key("src/auth.rs"),
"expected key 'src/auth.rs', got keys: {:?}",
hashes.keys().collect::<Vec<_>>()
);
assert!(
hashes.contains_key("src/middleware.rs"),
"expected key 'src/middleware.rs', got keys: {:?}",
hashes.keys().collect::<Vec<_>>()
);
assert_eq!(hashes.len(), 2, "expected exactly 2 keys for DIFF_V1");
}
#[test]
fn test_delta_new_files() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v2 = compute_all_file_hashes(&diff::parse(DIFF_V2));
let delta = compute_diff_delta(&hashes_v2, &hashes_v1);
assert!(
delta.new_files.contains(&"src/router.rs".to_string()),
"router.rs should be in new_files, got: {:?}",
delta.new_files
);
}
#[test]
fn test_delta_removed_files() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v4 = compute_all_file_hashes(&diff::parse(DIFF_V4));
let delta = compute_diff_delta(&hashes_v4, &hashes_v1);
assert!(
delta.removed_files.contains(&"src/auth.rs".to_string()),
"auth.rs should be in removed_files, got: {:?}",
delta.removed_files
);
}
#[test]
fn test_delta_modified_files() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v3 = compute_all_file_hashes(&diff::parse(DIFF_V3));
let delta = compute_diff_delta(&hashes_v3, &hashes_v1);
assert!(
delta.modified_files.contains(&"src/auth.rs".to_string()),
"auth.rs should be in modified_files, got: {:?}",
delta.modified_files
);
}
#[test]
fn test_delta_unchanged_files() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v3 = compute_all_file_hashes(&diff::parse(DIFF_V3));
let delta = compute_diff_delta(&hashes_v3, &hashes_v1);
assert!(
delta.unchanged_files.contains(&"src/middleware.rs".to_string()),
"middleware.rs should be unchanged, got: {:?}",
delta.unchanged_files
);
}
#[test]
fn test_delta_mixed() {
let mut old_hashes: HashMap<String, u64> = HashMap::new();
old_hashes.insert("file_a.rs".to_string(), 100);
old_hashes.insert("file_b.rs".to_string(), 200);
old_hashes.insert("file_c.rs".to_string(), 300);
let mut new_hashes: HashMap<String, u64> = HashMap::new();
new_hashes.insert("file_a.rs".to_string(), 100); new_hashes.insert("file_b.rs".to_string(), 999); new_hashes.insert("file_d.rs".to_string(), 400);
let delta = compute_diff_delta(&new_hashes, &old_hashes);
assert!(delta.unchanged_files.contains(&"file_a.rs".to_string()));
assert!(delta.modified_files.contains(&"file_b.rs".to_string()));
assert!(delta.new_files.contains(&"file_d.rs".to_string()));
assert!(delta.removed_files.contains(&"file_c.rs".to_string()));
}
#[test]
fn test_delta_has_changes_true() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v3 = compute_all_file_hashes(&diff::parse(DIFF_V3));
let delta = compute_diff_delta(&hashes_v3, &hashes_v1);
assert!(
delta.has_changes(),
"has_changes should be true when files are modified"
);
}
#[test]
fn test_delta_has_changes_false() {
let hashes = compute_all_file_hashes(&diff::parse(DIFF_V1));
let delta = compute_diff_delta(&hashes, &hashes);
assert!(
!delta.has_changes(),
"has_changes should be false for identical snapshots"
);
}
#[test]
fn test_delta_is_only_removals() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v4 = compute_all_file_hashes(&diff::parse(DIFF_V4));
let delta = compute_diff_delta(&hashes_v4, &hashes_v1);
assert!(
delta.is_only_removals(),
"is_only_removals should be true when only files removed, delta: {:?}",
delta
);
}
#[test]
fn test_delta_is_only_removals_false() {
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v2 = compute_all_file_hashes(&diff::parse(DIFF_V2));
let delta = compute_diff_delta(&hashes_v2, &hashes_v1);
assert!(
!delta.is_only_removals(),
"is_only_removals should be false when there are additions"
);
}
#[test]
fn test_normalize_fills_multi_hunk_empty() {
let data = diff::parse(DIFF_V1);
let mut groups = vec![SemanticGroup::new(
"Auth".to_string(),
String::new(),
vec![GroupedChange {
file: "src/auth.rs".to_string(),
hunks: vec![], }],
)];
normalize_hunk_indices(&mut groups, &data);
let changes = groups[0].changes();
assert_eq!(
changes[0].hunks,
vec![0, 1],
"empty hunks on a 2-hunk file should become [0, 1]"
);
}
#[test]
fn test_normalize_leaves_single_hunk() {
let data = diff::parse(DIFF_V1);
let mut groups = vec![SemanticGroup::new(
"Middleware".to_string(),
String::new(),
vec![GroupedChange {
file: "src/middleware.rs".to_string(),
hunks: vec![], }],
)];
normalize_hunk_indices(&mut groups, &data);
let changes = groups[0].changes();
assert!(
changes[0].hunks.is_empty(),
"single-hunk file should keep empty hunks list, got: {:?}",
changes[0].hunks
);
}
#[test]
fn test_normalize_preserves_explicit() {
let data = diff::parse(DIFF_V1);
let mut groups = vec![SemanticGroup::new(
"Auth login only".to_string(),
String::new(),
vec![GroupedChange {
file: "src/auth.rs".to_string(),
hunks: vec![0], }],
)];
normalize_hunk_indices(&mut groups, &data);
let changes = groups[0].changes();
assert_eq!(
changes[0].hunks,
vec![0],
"explicit hunks should not be modified by normalize"
);
}
#[test]
fn test_remove_files_basic() {
let mut groups = vec![make_group_multi(
"Auth group",
vec![("src/auth.rs", vec![0]), ("src/middleware.rs", vec![0])],
)];
remove_files_from_groups(&mut groups, &["src/auth.rs".to_string()]);
let changes = groups[0].changes();
assert_eq!(changes.len(), 1, "should have 1 change left");
assert_eq!(changes[0].file, "src/middleware.rs");
}
#[test]
fn test_remove_files_drops_empty_groups() {
let mut groups = vec![
make_group("Auth group", "src/auth.rs", vec![0]),
make_group("Middleware", "src/middleware.rs", vec![0]),
];
remove_files_from_groups(&mut groups, &["src/auth.rs".to_string()]);
assert_eq!(groups.len(), 1, "empty group should be removed");
assert_eq!(groups[0].label, "Middleware");
}
#[test]
fn test_remove_files_no_match() {
let mut groups = vec![make_group("Auth", "src/auth.rs", vec![0])];
remove_files_from_groups(&mut groups, &["src/nonexistent.rs".to_string()]);
assert_eq!(groups.len(), 1, "groups should be unchanged");
let changes = groups[0].changes();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].file, "src/auth.rs");
}
#[test]
fn test_merge_matching_label() {
let existing = vec![make_group("Auth Refactor", "src/auth.rs", vec![0])];
let new_assignments = vec![make_group("Auth Refactor", "src/router.rs", vec![0])];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string()],
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert_eq!(merged.len(), 1, "should still have 1 group after merging same label");
let changes = merged[0].changes();
let files: Vec<&str> = changes.iter().map(|c| c.file.as_str()).collect();
assert!(files.contains(&"src/auth.rs"), "existing file should be retained");
assert!(files.contains(&"src/router.rs"), "new file should be added");
}
#[test]
fn test_merge_new_label() {
let existing = vec![make_group("Auth", "src/auth.rs", vec![0])];
let new_assignments = vec![make_group("Routing", "src/router.rs", vec![0])];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string()],
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert_eq!(merged.len(), 2, "should have 2 groups (existing + new)");
let labels: Vec<&str> = merged.iter().map(|g| g.label.as_str()).collect();
assert!(labels.contains(&"Auth"), "original group should still exist");
assert!(labels.contains(&"Routing"), "new group should be added");
}
#[test]
fn test_merge_removes_stale() {
let existing = vec![make_group("Auth", "src/auth.rs", vec![0])];
let new_assignments = vec![make_group("Auth", "src/auth.rs", vec![0, 1])];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec![],
removed_files: vec![],
modified_files: vec!["src/auth.rs".to_string()],
unchanged_files: vec![],
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert_eq!(merged.len(), 1);
let changes = merged[0].changes();
assert_eq!(
changes[0].hunks,
vec![0, 1],
"merged group should have updated hunks from new assignment"
);
}
#[test]
fn test_merge_drops_empty() {
let existing = vec![make_group("Auth", "src/auth.rs", vec![0])];
let new_assignments: Vec<SemanticGroup> = vec![];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec![],
removed_files: vec!["src/auth.rs".to_string()],
modified_files: vec![],
unchanged_files: vec![],
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert!(
merged.is_empty(),
"empty groups after stale removal should be dropped, got: {:?}",
merged.iter().map(|g| &g.label).collect::<Vec<_>>()
);
}
#[test]
fn test_merge_case_insensitive_label() {
let existing = vec![make_group("Auth Refactor", "src/auth.rs", vec![0])];
let new_assignments = vec![make_group("auth refactor", "src/router.rs", vec![0])];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string()],
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert_eq!(
merged.len(),
1,
"case-insensitive label match should merge into existing group"
);
let changes = merged[0].changes();
let files: Vec<&str> = changes.iter().map(|c| c.file.as_str()).collect();
assert!(
files.contains(&"src/router.rs"),
"new file should be merged into the existing group"
);
}
#[test]
fn test_incremental_summaries_includes_context() {
let data = diff::parse(DIFF_V2);
let existing = vec![make_group_multi(
"Auth Refactor",
vec![
("src/auth.rs", vec![0, 1]),
("src/middleware.rs", vec![0]),
],
)];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string(), "src/middleware.rs".to_string()],
};
let output = incremental_hunk_summaries(&data, &delta, &existing);
assert!(
output.contains("EXISTING GROUPS"),
"output should contain EXISTING GROUPS section, got:\n{}",
output
);
assert!(
output.contains("Auth Refactor"),
"output should mention the existing group label"
);
}
#[test]
fn test_incremental_summaries_only_delta_files() {
let data = diff::parse(DIFF_V2);
let existing = vec![make_group("Auth", "src/auth.rs", vec![0, 1])];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string(), "src/middleware.rs".to_string()],
};
let output = incremental_hunk_summaries(&data, &delta, &existing);
assert!(
output.contains("src/router.rs"),
"router.rs (new file) should appear in output"
);
}
#[test]
fn test_incremental_summaries_skips_unchanged() {
let data = diff::parse(DIFF_V3);
let hashes_v1 = compute_all_file_hashes(&diff::parse(DIFF_V1));
let hashes_v3 = compute_all_file_hashes(&data);
let delta = compute_diff_delta(&hashes_v3, &hashes_v1);
let existing = vec![make_group_multi(
"Auth",
vec![
("src/auth.rs", vec![0, 1]),
("src/middleware.rs", vec![0]),
],
)];
let output = incremental_hunk_summaries(&data, &delta, &existing);
assert!(
!output.contains("FILE: src/middleware.rs"),
"unchanged file middleware.rs should not appear as a FILE: entry, output:\n{}",
output
);
assert!(
output.contains("FILE: src/auth.rs"),
"modified file auth.rs should appear as a FILE: entry"
);
}
#[test]
fn test_grouping_complete_sets_incremental_state() {
let _lock = PATH_MUTEX.lock().unwrap();
let original_path = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", "/nonexistent_test_dir");
let diff_data = diff::parse(DIFF_V1);
let config = Config::default_config();
let mut app = App::new(diff_data, &config, vec![]);
std::env::set_var("PATH", &original_path);
let (tx, _rx) = tokio::sync::mpsc::channel(32);
app.event_tx = Some(tx);
assert!(app.previous_file_hashes.is_empty());
let groups = vec![make_group("Auth", "src/auth.rs", vec![0, 1])];
app.update(Message::GroupingComplete(groups, 12345));
assert_eq!(
app.grouping_status,
GroupingStatus::Done,
"grouping_status should be Done after GroupingComplete"
);
assert!(
!app.previous_file_hashes.is_empty(),
"previous_file_hashes should be populated after GroupingComplete"
);
assert!(
app.semantic_groups.is_some(),
"semantic_groups should be set after GroupingComplete"
);
}
#[test]
fn test_incremental_complete_merges() {
let _lock = PATH_MUTEX.lock().unwrap();
let original_path = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", "/nonexistent_test_dir");
let diff_data = diff::parse(DIFF_V2);
let config = Config::default_config();
let mut app = App::new(diff_data, &config, vec![]);
std::env::set_var("PATH", &original_path);
let (tx, _rx) = tokio::sync::mpsc::channel(32);
app.event_tx = Some(tx);
app.semantic_groups = Some(vec![make_group_multi(
"Auth Refactor",
vec![
("src/auth.rs", vec![0, 1]),
("src/middleware.rs", vec![0]),
],
)]);
let new_assignments = vec![make_group("Auth Refactor", "src/router.rs", vec![0])];
let new_hashes = compute_all_file_hashes(&app.diff_data);
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/router.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec!["src/auth.rs".to_string(), "src/middleware.rs".to_string()],
};
app.update(Message::IncrementalGroupingComplete(
new_assignments,
delta,
new_hashes,
99999,
"abc123".to_string(),
));
assert_eq!(
app.grouping_status,
GroupingStatus::Done,
"grouping_status should be Done after IncrementalGroupingComplete"
);
let groups = app.semantic_groups.as_ref().unwrap();
assert_eq!(groups.len(), 1, "should have 1 merged group");
let group_changes = groups[0].changes();
let files: Vec<&str> = group_changes.iter().map(|c| c.file.as_str()).collect();
assert!(
files.contains(&"src/router.rs"),
"merged group should contain router.rs, files: {:?}",
files
);
assert!(
files.contains(&"src/auth.rs"),
"merged group should retain auth.rs, files: {:?}",
files
);
}
#[test]
fn test_incremental_complete_normalizes_hunks() {
let _lock = PATH_MUTEX.lock().unwrap();
let original_path = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", "/nonexistent_test_dir");
let diff_data = diff::parse(DIFF_V1);
let config = Config::default_config();
let mut app = App::new(diff_data, &config, vec![]);
std::env::set_var("PATH", &original_path);
let (tx, _rx) = tokio::sync::mpsc::channel(32);
app.event_tx = Some(tx);
app.semantic_groups = Some(vec![]);
let new_assignments = vec![SemanticGroup::new(
"Auth".to_string(),
String::new(),
vec![GroupedChange {
file: "src/auth.rs".to_string(),
hunks: vec![], }],
)];
let new_hashes = compute_all_file_hashes(&app.diff_data);
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec!["src/auth.rs".to_string()],
removed_files: vec![],
modified_files: vec![],
unchanged_files: vec![],
};
app.update(Message::IncrementalGroupingComplete(
new_assignments,
delta,
new_hashes,
11111,
"def456".to_string(),
));
let groups = app.semantic_groups.as_ref().unwrap();
assert_eq!(groups.len(), 1);
let changes = groups[0].changes();
let auth_change = changes
.iter()
.find(|c| c.file == "src/auth.rs")
.expect("auth.rs should be in changes");
assert_eq!(
auth_change.hunks,
vec![0, 1],
"normalize should fill in [0, 1] for auth.rs (2-hunk file)"
);
}
fn generate_diff_n_files(num_files: usize) -> String {
let mut out = String::new();
for i in 0..num_files {
out.push_str(&format!(
"diff --git a/src/file_{i}.rs b/src/file_{i}.rs\n\
--- a/src/file_{i}.rs\n\
+++ b/src/file_{i}.rs\n\
@@ -1,2 +1,3 @@ fn f_{i}\n\
fn f_{i}() {{\n\
+ new_call_{i}();\n\
}}\n"
));
}
out
}
#[test]
fn test_stress_100_files_delta() {
let diff_old = generate_diff_n_files(50);
let diff_new = generate_diff_n_files(100);
let hashes_old = compute_all_file_hashes(&diff::parse(&diff_old));
let hashes_new = compute_all_file_hashes(&diff::parse(&diff_new));
let delta = compute_diff_delta(&hashes_new, &hashes_old);
assert_eq!(
delta.new_files.len(),
50,
"expected 50 new files, got: {}",
delta.new_files.len()
);
assert_eq!(
delta.unchanged_files.len(),
50,
"expected 50 unchanged files, got: {}",
delta.unchanged_files.len()
);
assert!(
delta.removed_files.is_empty(),
"expected no removed files, got: {}",
delta.removed_files.len()
);
assert!(
delta.modified_files.is_empty(),
"expected no modified files, got: {}",
delta.modified_files.len()
);
}
#[test]
fn test_stress_merge_20_groups() {
let existing: Vec<SemanticGroup> = (0..20)
.map(|i| make_group(&format!("Group {i}"), &format!("src/file_{i}.rs"), vec![0]))
.collect();
let new_assignments: Vec<SemanticGroup> = vec![
make_group("Group 0", "src/router.rs", vec![0]),
make_group("Group 5", "src/new_a.rs", vec![0]),
make_group("Group 10", "src/new_b.rs", vec![0]),
make_group("Brand New X", "src/brand_x.rs", vec![0]),
make_group("Brand New Y", "src/brand_y.rs", vec![0]),
];
let delta = semantic_diff::grouper::DiffDelta {
new_files: vec![
"src/router.rs".to_string(),
"src/new_a.rs".to_string(),
"src/new_b.rs".to_string(),
"src/brand_x.rs".to_string(),
"src/brand_y.rs".to_string(),
],
removed_files: vec![],
modified_files: vec![],
unchanged_files: (0..20).map(|i| format!("src/file_{i}.rs")).collect(),
};
let merged = merge_groups(&existing, &new_assignments, &delta);
assert_eq!(
merged.len(),
22,
"expected 22 groups (20 original + 2 brand new), got: {}",
merged.len()
);
let group_0 = merged.iter().find(|g| g.label == "Group 0").unwrap();
let group_0_changes = group_0.changes();
let files_0: Vec<&str> = group_0_changes
.iter()
.map(|c| c.file.as_str())
.collect();
assert!(
files_0.contains(&"src/file_0.rs"),
"Group 0 should still contain file_0.rs"
);
assert!(
files_0.contains(&"src/router.rs"),
"Group 0 should now also contain router.rs"
);
}
#[test]
fn test_stress_normalize_100_files() {
let mut raw = String::new();
for i in 0..100 {
raw.push_str(&format!(
"diff --git a/src/file_{i}.rs b/src/file_{i}.rs\n\
--- a/src/file_{i}.rs\n\
+++ b/src/file_{i}.rs\n\
@@ -1,3 +1,4 @@ fn first_{i}()\n \
fn first_{i}() {{\n\
+ call_a_{i}();\n \
}}\n\
@@ -10,3 +11,4 @@ fn second_{i}()\n \
fn second_{i}() {{\n\
+ call_b_{i}();\n \
}}\n"
));
}
let data = diff::parse(&raw);
let mut groups: Vec<SemanticGroup> = (0..100)
.map(|i| {
SemanticGroup::new(
format!("Group {i}"),
String::new(),
vec![GroupedChange {
file: format!("src/file_{i}.rs"),
hunks: vec![],
}],
)
})
.collect();
normalize_hunk_indices(&mut groups, &data);
let mut normalized_count = 0;
for group in &groups {
for change in group.changes() {
if change.hunks == vec![0, 1] {
normalized_count += 1;
}
}
}
assert!(
normalized_count >= 90,
"at least 90 of 100 files should have normalized hunks [0,1], got: {}",
normalized_count
);
}
#[test]
fn test_stress_rapid_incremental() {
let base_diff = generate_diff_n_files(5);
let base_data = diff::parse(&base_diff);
let mut current_hashes = compute_all_file_hashes(&base_data);
let mut current_groups: Vec<SemanticGroup> = (0..5)
.map(|i| make_group(&format!("Group {i}"), &format!("src/file_{i}.rs"), vec![0]))
.collect();
for round in 0..10usize {
let new_file_idx = 5 + round;
let mut new_hashes = current_hashes.clone();
new_hashes.insert(format!("src/file_{new_file_idx}.rs"), round as u64 + 1000);
let delta = compute_diff_delta(&new_hashes, ¤t_hashes);
assert_eq!(
delta.new_files.len(),
1,
"round {round}: expected exactly 1 new file"
);
assert!(
delta.removed_files.is_empty(),
"round {round}: expected no removed files"
);
assert!(
delta.modified_files.is_empty(),
"round {round}: expected no modified files"
);
assert!(
delta.has_changes(),
"round {round}: delta should have changes"
);
assert!(
!delta.is_only_removals(),
"round {round}: adding a file is not only removals"
);
let new_assignment = vec![make_group(
&format!("New Group {round}"),
&format!("src/file_{new_file_idx}.rs"),
vec![0],
)];
current_groups = merge_groups(¤t_groups, &new_assignment, &delta);
current_hashes = new_hashes;
assert_eq!(
current_groups.len(),
5 + round + 1,
"round {round}: expected {} groups after merge",
5 + round + 1
);
}
assert_eq!(current_groups.len(), 15);
}