huskies: merge 860
This commit is contained in:
+221
-4
@@ -212,6 +212,33 @@ fn run_command_with_timeout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the documentation coverage gate for the given worktree.
|
||||||
|
///
|
||||||
|
/// Invokes `cargo run --quiet -p source-map-gen --bin source-map-check` in
|
||||||
|
/// `path`, comparing the current branch against its detected base branch
|
||||||
|
/// (`master` or `main`). Returns `(passed, combined_output)`.
|
||||||
|
pub(crate) fn run_doc_coverage_gate(path: &Path) -> Result<(bool, String), String> {
|
||||||
|
let base = detect_worktree_base_branch(path);
|
||||||
|
let base_str = base.as_str();
|
||||||
|
run_command_with_timeout(
|
||||||
|
"cargo",
|
||||||
|
&[
|
||||||
|
"run",
|
||||||
|
"--quiet",
|
||||||
|
"-p",
|
||||||
|
"source-map-gen",
|
||||||
|
"--bin",
|
||||||
|
"source-map-check",
|
||||||
|
"--",
|
||||||
|
"--worktree",
|
||||||
|
".",
|
||||||
|
"--base",
|
||||||
|
base_str,
|
||||||
|
],
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `cargo clippy` and the project test suite (via `script/test` if present,
|
/// Run `cargo clippy` and the project test suite (via `script/test` if present,
|
||||||
/// otherwise `cargo nextest run` / `cargo test`) in the given directory.
|
/// otherwise `cargo nextest run` / `cargo test`) in the given directory.
|
||||||
/// Returns `(gates_passed, combined_output)`.
|
/// Returns `(gates_passed, combined_output)`.
|
||||||
@@ -234,12 +261,27 @@ pub(crate) fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String
|
|||||||
return Ok((false, msg));
|
return Ok((false, msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run script/test (or fallback to cargo test). This is the sole
|
// Run script/test (or fallback to cargo test). Project-specific linting
|
||||||
// acceptance gate — project-specific linting and test commands belong
|
// and test commands belong in script/test.
|
||||||
// in script/test, not hardcoded here.
|
|
||||||
let (test_success, test_out) = run_project_tests(path)?;
|
let (test_success, test_out) = run_project_tests(path)?;
|
||||||
|
if !test_success {
|
||||||
|
return Ok((false, test_out));
|
||||||
|
}
|
||||||
|
|
||||||
Ok((test_success, test_out))
|
// Doc coverage gate (defence in depth): verify all public items in files
|
||||||
|
// changed since the base branch have doc comments. This gate catches
|
||||||
|
// projects that do not include source-map-check in their script/test, and
|
||||||
|
// provides an actionable "Doc coverage gate failed:" preamble so the agent
|
||||||
|
// knows exactly what to fix on retry.
|
||||||
|
let (doc_ok, doc_out) = run_doc_coverage_gate(path)?;
|
||||||
|
if !doc_ok {
|
||||||
|
return Ok((
|
||||||
|
false,
|
||||||
|
format!("{test_out}Doc coverage gate failed:\n{doc_out}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((true, test_out))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan `root` recursively for Rust source files where both `path/X.rs` and
|
/// Scan `root` recursively for Rust source files where both `path/X.rs` and
|
||||||
@@ -557,6 +599,181 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── run_acceptance_gates doc-coverage tests ───────────────────────────────
|
||||||
|
|
||||||
|
/// Create a git worktree from the actual huskies workspace on a fresh
|
||||||
|
/// branch (`<branch>`) rooted at `master`. Returns the path to the
|
||||||
|
/// worktree. Call `cleanup_worktree` when done.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn create_test_worktree(branch: &str) -> std::path::PathBuf {
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf();
|
||||||
|
let wt = std::env::temp_dir().join(branch.replace('/', "_"));
|
||||||
|
// Remove any stale worktree from a previous failed run.
|
||||||
|
let _ = Command::new("git")
|
||||||
|
.args(["worktree", "remove", "--force", wt.to_str().unwrap()])
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.output();
|
||||||
|
let _ = Command::new("git")
|
||||||
|
.args(["branch", "-D", branch])
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let out = Command::new("git")
|
||||||
|
.args([
|
||||||
|
"worktree",
|
||||||
|
"add",
|
||||||
|
wt.to_str().unwrap(),
|
||||||
|
"-b",
|
||||||
|
branch,
|
||||||
|
"master",
|
||||||
|
])
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.output()
|
||||||
|
.expect("git worktree add");
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"git worktree add failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
wt
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the test worktree and its branch from the workspace.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn cleanup_worktree(branch: &str, wt: &std::path::Path) {
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf();
|
||||||
|
let _ = Command::new("git")
|
||||||
|
.args(["worktree", "remove", "--force", wt.to_str().unwrap()])
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.output();
|
||||||
|
let _ = Command::new("git")
|
||||||
|
.args(["branch", "-D", branch])
|
||||||
|
.current_dir(&workspace)
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a `.cargo/config.toml` inside `wt` that redirects the build
|
||||||
|
/// target directory to the shared workspace `target/` so `cargo run`
|
||||||
|
/// finds the already-compiled `source-map-check` binary without
|
||||||
|
/// recompiling.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn set_shared_target(wt: &std::path::Path) {
|
||||||
|
use std::fs;
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf();
|
||||||
|
let target_dir = workspace.join("target");
|
||||||
|
let cargo_dir = wt.join(".cargo");
|
||||||
|
fs::create_dir_all(&cargo_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
cargo_dir.join("config.toml"),
|
||||||
|
format!(
|
||||||
|
"[build]\ntarget-dir = \"{}\"\n",
|
||||||
|
target_dir.to_str().unwrap()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write and chmod a minimal `script/test` that exits 0 without running
|
||||||
|
/// the full test suite. The file is left uncommitted so `run_project_tests`
|
||||||
|
/// uses it but it does not appear in the doc-check diff.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn write_fast_script_test(wt: &std::path::Path) {
|
||||||
|
use std::fs;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let script = wt.join("script").join("test");
|
||||||
|
fs::write(&script, "#!/usr/bin/env bash\nexit 0\n").unwrap();
|
||||||
|
let mut perms = fs::metadata(&script).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&script, perms).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Commit a single Rust file in the worktree.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn commit_file(wt: &std::path::Path, rel_path: &str, content: &str) {
|
||||||
|
use std::fs;
|
||||||
|
let full = wt.join(rel_path);
|
||||||
|
if let Some(parent) = full.parent() {
|
||||||
|
fs::create_dir_all(parent).unwrap();
|
||||||
|
}
|
||||||
|
fs::write(&full, content).unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args(["add", rel_path])
|
||||||
|
.current_dir(wt)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
Command::new("git")
|
||||||
|
.args([
|
||||||
|
"-c",
|
||||||
|
"user.email=test@test.com",
|
||||||
|
"-c",
|
||||||
|
"user.name=Test",
|
||||||
|
"commit",
|
||||||
|
"-m",
|
||||||
|
"test: add file for doc gate test",
|
||||||
|
])
|
||||||
|
.current_dir(wt)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn run_acceptance_gates_fails_on_undocumented_pub_fn() {
|
||||||
|
let branch = format!("test/doc-gate-fail-story-860-{}", std::process::id());
|
||||||
|
let wt = create_test_worktree(&branch);
|
||||||
|
|
||||||
|
set_shared_target(&wt);
|
||||||
|
write_fast_script_test(&wt);
|
||||||
|
commit_file(
|
||||||
|
&wt,
|
||||||
|
"server/src/_doc_gate_test_undoc.rs",
|
||||||
|
"pub fn undocumented_gate_test_fn() {}\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = run_acceptance_gates(&wt);
|
||||||
|
cleanup_worktree(&branch, &wt);
|
||||||
|
|
||||||
|
let (passed, output) = result.unwrap();
|
||||||
|
assert!(!passed, "acceptance gates should fail; output:\n{output}");
|
||||||
|
assert!(
|
||||||
|
output.contains("Doc coverage gate failed:"),
|
||||||
|
"output should contain 'Doc coverage gate failed:'; got:\n{output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn run_acceptance_gates_passes_with_documented_pub_fn() {
|
||||||
|
let branch = format!("test/doc-gate-pass-story-860-{}", std::process::id());
|
||||||
|
let wt = create_test_worktree(&branch);
|
||||||
|
|
||||||
|
set_shared_target(&wt);
|
||||||
|
write_fast_script_test(&wt);
|
||||||
|
commit_file(
|
||||||
|
&wt,
|
||||||
|
"server/src/_doc_gate_test_doc.rs",
|
||||||
|
"//! Test module for the doc-coverage happy-path gate test.\n/// A test function that is properly documented.\npub fn documented_gate_test_fn() {}\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = run_acceptance_gates(&wt);
|
||||||
|
cleanup_worktree(&branch, &wt);
|
||||||
|
|
||||||
|
let (passed, output) = result.unwrap();
|
||||||
|
assert!(
|
||||||
|
passed,
|
||||||
|
"acceptance gates should pass for documented pub fn; output:\n{output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── worktree_has_committed_work tests ─────────────────────────────────────
|
// ── worktree_has_committed_work tests ─────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user