huskies: merge 851

This commit is contained in:
dave
2026-04-29 08:38:00 +00:00
parent 3ce34c34e9
commit 549a9defc4
3 changed files with 163 additions and 4 deletions
+138
View File
@@ -216,6 +216,24 @@ fn run_command_with_timeout(
/// otherwise `cargo nextest run` / `cargo test`) in the given directory.
/// Returns `(gates_passed, combined_output)`.
pub(crate) fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String> {
// Pre-flight: detect duplicate module files (E0761) before running the
// full test suite so the failure message is immediately actionable.
let duplicates = find_duplicate_module_files(path);
if !duplicates.is_empty() {
let mut msg = String::from(
"ERROR [E0761]: duplicate module files detected — cargo will fail to compile.\n\
Fix: git rm the flat .rs file in the same commit that introduces the mod.rs.\n",
);
for (flat, mod_path) in &duplicates {
msg.push_str(&format!(
" {} (conflicts with {})\n",
flat.display(),
mod_path.display()
));
}
return Ok((false, msg));
}
// Run script/test (or fallback to cargo test). This is the sole
// acceptance gate — project-specific linting and test commands belong
// in script/test, not hardcoded here.
@@ -224,6 +242,47 @@ pub(crate) fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String
Ok((test_success, test_out))
}
/// Scan `root` recursively for Rust source files where both `path/X.rs` and
/// `path/X/mod.rs` exist — a condition that produces a `duplicate module file`
/// cargo error (E0761).
///
/// Returns a sorted list of `(flat_path, mod_path)` pairs in conflict.
/// Directories named `target` or starting with `.` are skipped.
pub(crate) fn find_duplicate_module_files(
root: &Path,
) -> Vec<(std::path::PathBuf, std::path::PathBuf)> {
let mut duplicates = Vec::new();
find_duplicates_recursive(root, &mut duplicates);
duplicates.sort();
duplicates
}
fn find_duplicates_recursive(dir: &Path, out: &mut Vec<(std::path::PathBuf, std::path::PathBuf)>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_owned(),
None => continue,
};
if name == "target" || name.starts_with('.') {
continue;
}
let flat = dir.join(format!("{name}.rs"));
let mod_rs = path.join("mod.rs");
if flat.exists() && mod_rs.exists() {
out.push((flat, mod_rs));
}
find_duplicates_recursive(&path, out);
}
}
/// Run `script/test_coverage` in the given directory if the script exists.
///
/// Used as a QA gate before advancing a story from `3_qa/` to `4_merge/`.
@@ -289,6 +348,85 @@ mod tests {
.unwrap();
}
// ── find_duplicate_module_files tests ────────────────────────
#[test]
fn find_duplicate_module_files_returns_empty_when_no_duplicates() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
// Only X/mod.rs, no X.rs
let sub = src.join("util");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("mod.rs"), "").unwrap();
let result = find_duplicate_module_files(tmp.path());
assert!(result.is_empty(), "no duplicates expected: {result:?}");
}
#[test]
fn find_duplicate_module_files_detects_flat_and_mod_rs() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
// Create both src/config.rs and src/config/mod.rs
fs::write(src.join("config.rs"), "").unwrap();
let config_dir = src.join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("mod.rs"), "").unwrap();
let result = find_duplicate_module_files(tmp.path());
assert_eq!(
result.len(),
1,
"expected exactly one duplicate: {result:?}"
);
let (flat, mod_path) = &result[0];
assert!(
flat.ends_with("src/config.rs"),
"flat path should be config.rs, got {flat:?}"
);
assert!(
mod_path.ends_with("src/config/mod.rs"),
"mod path should be config/mod.rs, got {mod_path:?}"
);
}
#[test]
fn find_duplicate_module_files_skips_target_directory() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
// Duplicate inside target/ should be ignored
let target = tmp.path().join("target").join("debug").join("foo");
fs::create_dir_all(&target).unwrap();
fs::write(target.join("mod.rs"), "").unwrap();
fs::write(tmp.path().join("target").join("debug").join("foo.rs"), "").unwrap();
let result = find_duplicate_module_files(tmp.path());
assert!(
result.is_empty(),
"duplicates inside target/ should be ignored: {result:?}"
);
}
#[test]
fn find_duplicate_module_files_reports_both_paths_in_message() {
use std::fs;
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("handlers.rs"), "").unwrap();
let handlers_dir = src.join("handlers");
fs::create_dir_all(&handlers_dir).unwrap();
fs::write(handlers_dir.join("mod.rs"), "").unwrap();
let result = find_duplicate_module_files(tmp.path());
assert_eq!(result.len(), 1);
let (flat, mod_path) = &result[0];
// Both file names must appear in the paths so a caller can surface them
assert!(flat.to_string_lossy().contains("handlers.rs"));
assert!(mod_path.to_string_lossy().contains("handlers"));
assert!(mod_path.to_string_lossy().contains("mod.rs"));
}
// ── run_project_tests tests ───────────────────────────────────
#[cfg(unix)]