mod common;
use common::Sandbox;
use std::fs;
use std::process::Command;
fn git(dir: &std::path::Path, args: &[&str]) -> std::process::Output {
Command::new("git")
.current_dir(dir)
.args(args)
.output()
.expect("git")
}
fn fresh_git_repo() -> Sandbox {
let s = Sandbox::new();
s.init("doctor-test");
let _ = git(s.dir.path(), &["init", "-q", "-b", "main"]);
let _ = git(s.dir.path(), &["config", "user.email", "t@example.com"]);
let _ = git(s.dir.path(), &["config", "user.name", "Tester"]);
let _ = git(s.dir.path(), &["config", "commit.gpgsign", "false"]);
s
}
#[test]
fn req_0064_doctor_reports_missing_pre_commit() {
let s = fresh_git_repo();
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args(["--file", s.path().to_str().unwrap(), "doctor"])
.output()
.expect("invoke req");
assert!(
!out.status.success(),
"doctor should fail when nothing is configured"
);
let body = String::from_utf8_lossy(&out.stdout);
assert!(body.contains("pre-commit hook"));
assert!(body.contains("FAIL"));
}
#[test]
fn req_0064_doctor_passes_after_hooks_install() {
let s = fresh_git_repo();
let install = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args([
"--file",
s.path().to_str().unwrap(),
"hooks",
"install",
"--force",
])
.output()
.expect("hooks install");
assert!(
install.status.success(),
"hooks install: {}",
String::from_utf8_lossy(&install.stderr)
);
let _ = git(
s.dir.path(),
&[
"config",
"merge.req-merge.driver",
"req renumber --base %O || true",
],
);
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args(["--file", s.path().to_str().unwrap(), "doctor", "--json"])
.output()
.expect("invoke req");
let body = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(&body).expect("doctor --json shape");
let checks = v["checks"].as_array().expect("checks array");
let hook = checks
.iter()
.find(|c| c["name"] == "pre-commit hook")
.unwrap();
assert!(hook["ok"].as_bool().unwrap(), "pre-commit should be OK");
let pin = checks
.iter()
.find(|c| c["name"] == "gitattributes line-ending pin")
.unwrap();
assert!(
pin["ok"].as_bool().unwrap(),
"gitattributes pin should be OK"
);
}
#[test]
fn req_0069_diff_reports_added_and_changed() {
let s = fresh_git_repo();
let add1 = s.run(&[
"add",
"--title",
"Baseline requirement here",
"--statement",
"The system shall start with this established baseline.",
"--rationale",
"Setup.",
"--kind",
"constraint",
"--priority",
"could",
]);
assert!(add1.status.success());
let _ = git(s.dir.path(), &["add", "project.req"]);
let _ = git(s.dir.path(), &["commit", "-q", "-m", "baseline"]);
let add2 = s.run(&[
"add",
"--title",
"Second requirement appears now",
"--statement",
"The system shall now also do this additional thing.",
"--rationale",
"Added.",
"--kind",
"constraint",
"--priority",
"could",
]);
assert!(add2.status.success());
for status in ["proposed", "approved", "implemented"] {
let upd = s.run(&[
"update",
"REQ-0001",
"--status",
status,
"--reason",
"Done in this branch",
]);
assert!(upd.status.success(), "step to {}", status);
}
let _ = git(s.dir.path(), &["add", "project.req"]);
let _ = git(s.dir.path(), &["commit", "-q", "-m", "head"]);
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args(["--file", s.path().to_str().unwrap(), "diff", "HEAD~1..HEAD"])
.output()
.expect("invoke req");
let body = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
body.contains("ADDED"),
"should report ADDED section: {}",
body
);
assert!(body.contains("REQ-0002"));
assert!(body.contains("CHANGED"));
assert!(body.contains("REQ-0001"));
assert!(body.contains("status:"));
}
#[test]
fn req_0069_diff_empty_when_no_changes() {
let s = fresh_git_repo();
s.run(&[
"add",
"--title",
"Single requirement only",
"--statement",
"The system shall have just this requirement, nothing more.",
"--rationale",
"Setup.",
"--kind",
"constraint",
"--priority",
"could",
]);
let _ = git(s.dir.path(), &["add", "project.req"]);
let _ = git(s.dir.path(), &["commit", "-q", "-m", "only"]);
let _ = git(
s.dir.path(),
&["commit", "-q", "--allow-empty", "-m", "empty"],
);
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.current_dir(s.dir.path())
.args(["--file", s.path().to_str().unwrap(), "diff", "HEAD~1..HEAD"])
.output()
.expect("req diff");
let body = String::from_utf8_lossy(&out.stdout);
assert!(body.contains("no requirement-level changes"));
}
#[test]
fn req_0070_test_only_marker_is_distinct_from_referenced() {
use crate::common as common_alias;
let _ = common_alias::req(&["--help"]);
let s = Sandbox::new();
s.init("p");
let _ = s.run(&[
"add",
"--title",
"Impl-only requirement",
"--statement",
"The system shall be referenced from src only.",
"--rationale",
"Test fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
let _ = s.run(&[
"add",
"--title",
"Test-only requirement",
"--statement",
"The system shall be referenced from tests only.",
"--rationale",
"Test fixture.",
"--kind",
"constraint",
"--priority",
"could",
]);
fs::create_dir_all(s.dir.path().join("src")).unwrap();
fs::create_dir_all(s.dir.path().join("tests")).unwrap();
fs::write(
s.dir.path().join("src/lib.rs"),
"// REQ-0001 implementation site\nfn _foo() {}\n",
)
.unwrap();
fs::write(
s.dir.path().join("tests/coverage_test.rs"),
"// REQ-0002 test-only reference\nfn _t() {}\n",
)
.unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"--file",
s.path().to_str().unwrap(),
"coverage",
"--path",
s.dir.path().to_str().unwrap(),
"--json",
])
.output()
.expect("coverage --json");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("coverage --json shape");
assert!(
v["referenced"]
.as_object()
.unwrap()
.contains_key("REQ-0001"),
"REQ-0001 should be referenced: {}",
v
);
assert!(
v["test_only"].as_object().unwrap().contains_key("REQ-0002"),
"REQ-0002 should be test-only: {}",
v
);
assert!(
!v["referenced"]
.as_object()
.unwrap()
.contains_key("REQ-0002"),
"REQ-0002 must NOT count as fully-referenced"
);
}