huskies: merge 851
This commit is contained in:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user