mod common;
use assert_cmd::Command;
use tempfile::TempDir;
fn drep() -> Command {
let mut command = Command::cargo_bin("drep").expect("binary builds");
command.env("DREP_SITE_CONFIG", absent_site_policy());
command
}
fn absent_site_policy() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("absent-site-policy.toml")
}
fn repo_with(name: &str, content: &str) -> TempDir {
let dir = TempDir::new().expect("temp dir");
std::fs::write(dir.path().join(name), content).expect("write");
dir
}
fn with_config(dir: &TempDir) {
std::fs::write(
dir.path().join("drep.toml"),
"[[llm]]\nmodel = \"m\"\nendpoint = \"http://127.0.0.1:59999/v1\"\n",
)
.expect("write config");
}
#[test]
fn version_succeeds() {
drep().arg("--version").assert().success();
}
#[test]
fn help_succeeds() {
drep().arg("--help").assert().success();
}
#[test]
fn lint_docs_is_report_only_by_default_and_gates_under_strict() {
let dir = repo_with("bad.md", "#Heading\n");
drep()
.arg("lint-docs")
.current_dir(dir.path())
.assert()
.code(0);
drep()
.args(["lint-docs", "--strict"])
.current_dir(dir.path())
.assert()
.code(1);
let clean = repo_with("good.md", "# Heading\n\nprose\n");
drep()
.args(["lint-docs", "--strict"])
.current_dir(clean.path())
.assert()
.code(0)
.stdout("No issues found.\n");
}
#[test]
fn a_named_file_no_command_can_analyze_never_exits_clean() {
let dir = repo_with("README.md", "# T\n\nprose\n");
std::fs::write(dir.path().join("main.rs"), "fn main() {}\n").expect("write");
with_config(&dir);
let assert = drep()
.args(["check", "README.md"])
.current_dir(dir.path())
.assert()
.code(2);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).expect("utf8");
assert!(stdout.contains("lint-docs"), "{stdout}");
let assert = drep()
.args(["lint-docs", "main.rs"])
.current_dir(dir.path())
.assert()
.code(2);
let stdout = String::from_utf8(assert.get_output().stdout.clone()).expect("utf8");
assert!(stdout.contains("drep check"), "{stdout}");
}
#[test]
fn an_unparseable_site_policy_exits_two_through_the_real_binary() {
let dir = repo_with("lib.py", "x = 1\n");
with_config(&dir);
let site = dir.path().join("site.toml");
std::fs::write(&site, "not toml at all\n").expect("write site policy");
let assert = drep()
.args(["check", "lib.py"])
.current_dir(dir.path())
.env("DREP_SITE_CONFIG", &site)
.assert()
.code(2);
let stderr = String::from_utf8(assert.get_output().stderr.clone()).expect("utf8");
assert!(
stderr.contains(&site.display().to_string()),
"a policy drep could not load is never a clean or a merely-blocked run, \
and the message has to name the file; got {stderr}"
);
}
#[test]
fn usage_error_also_blocks() {
drep().arg("no-such-command").assert().code(2);
drep()
.args(["check", "--fail-on", "critical"])
.assert()
.code(2);
drep().args(["check", "--staged", "a.rs"]).assert().code(2);
}
#[test]
fn no_command_is_a_usage_error() {
drep().assert().failure();
}
const CLEAN_README: &str = "# Title\n\nClean text.\n";
const BROKEN_README: &str = "# Title\n\nClean text.\n\n#Broken heading\n";
fn gated_repository() -> (TempDir, String) {
let dir = TempDir::new().expect("temp dir");
let root = dir.path();
common::git_init(root);
common::git_must(root, &["config", "--local", "core.hooksPath", "hooks"]);
std::fs::create_dir(root.join("hooks")).expect("hooks directory");
common::write_executable(
&root.join("hooks").join("pre-commit"),
&format!(
"#!/bin/sh\nDREP_SITE_CONFIG='{}' exec '{}' lint-docs --staged --strict\n",
absent_site_policy().display(),
env!("CARGO_BIN_EXE_drep")
),
);
std::fs::write(root.join("README.md"), CLEAN_README).expect("write the README");
common::git_must(root, &["add", "README.md"]);
common::git_must(root, &["commit", "--quiet", "--no-verify", "-m", "clean"]);
let head = common::git_must(root, &["rev-parse", "HEAD"]);
std::fs::write(root.join("README.md"), BROKEN_README).expect("break the README");
(dir, head)
}
fn assert_gate_refuses(
dir: &TempDir,
from: &std::path::Path,
head: &str,
extra: &[&str],
env: &[(&str, &std::path::Path)],
) {
let mut commit = common::without_outer_git("git", from);
commit
.args(["commit", "--quiet", "-m", "broken"])
.args(extra);
for (name, value) in env {
commit.env(name, value);
}
let output = commit.output().expect("git must run");
let said = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
!output.status.success(),
"the gate let the commit through: {said}"
);
assert!(
said.contains("missing_space_after_heading"),
"the gate named the issue: {said}"
);
assert_eq!(
common::git_must(dir.path(), &["rev-parse", "HEAD"]),
head,
"nothing was committed"
);
}
fn stage_into_index(from: &std::path::Path, index: &std::path::Path, path: &str) {
for args in [&["read-tree", "HEAD"][..], &["add", path][..]] {
let status = common::without_outer_git("git", from)
.env("GIT_INDEX_FILE", index)
.args(args)
.status()
.expect("git must run");
assert!(
status.success(),
"git {args:?} into {} failed",
index.display()
);
}
}
#[test]
fn the_staged_gate_reviews_a_commit_of_every_tracked_change() {
let (dir, head) = gated_repository();
assert_gate_refuses(&dir, dir.path(), &head, &["-a"], &[]);
}
#[test]
fn the_staged_gate_reviews_a_commit_of_named_paths() {
let (dir, head) = gated_repository();
assert_gate_refuses(&dir, dir.path(), &head, &["--", "README.md"], &[]);
}
#[test]
fn the_staged_gate_reviews_what_the_commit_records_not_the_working_tree() {
let (dir, head) = gated_repository();
common::git_must(dir.path(), &["add", "README.md"]);
std::fs::write(dir.path().join("README.md"), CLEAN_README).expect("restore the README");
assert_gate_refuses(&dir, dir.path(), &head, &[], &[]);
}
#[test]
fn the_staged_gate_reviews_a_chosen_index_not_the_working_tree() {
let (dir, head) = gated_repository();
let elsewhere = TempDir::new().expect("temp dir");
let index = elsewhere.path().join("alternate.index");
stage_into_index(dir.path(), &index, "README.md");
std::fs::write(dir.path().join("README.md"), CLEAN_README).expect("restore the README");
assert_gate_refuses(&dir, dir.path(), &head, &[], &[("GIT_INDEX_FILE", &index)]);
}
#[test]
fn the_staged_gate_reviews_a_relative_chosen_index_committed_from_a_subdirectory() {
let (dir, head) = gated_repository();
let sub = dir.path().join("sub");
std::fs::create_dir(&sub).expect("subdirectory");
stage_into_index(&sub, std::path::Path::new("commit.index"), "../README.md");
assert!(
dir.path().join("commit.index").is_file(),
"git wrote the relative index at the top level"
);
std::fs::write(dir.path().join("README.md"), CLEAN_README).expect("restore the README");
assert_gate_refuses(
&dir,
&sub,
&head,
&[],
&[("GIT_INDEX_FILE", std::path::Path::new("commit.index"))],
);
}
#[test]
fn the_staged_gate_passes_a_clean_commit_whatever_the_working_tree_holds() {
let (dir, head) = gated_repository();
let clean_edit = format!("{CLEAN_README}\nMore clean text.\n");
std::fs::write(dir.path().join("README.md"), &clean_edit).expect("edit the README");
common::git_must(dir.path(), &["add", "README.md"]);
std::fs::write(dir.path().join("README.md"), BROKEN_README).expect("break the working tree");
common::git_must(dir.path(), &["commit", "--quiet", "-m", "clean edit"]);
assert_ne!(common::git_must(dir.path(), &["rev-parse", "HEAD"]), head);
assert_eq!(
common::git_must(dir.path(), &["show", "HEAD:README.md"]),
clean_edit.trim_end()
);
}
#[test]
fn run_from_a_subdirectory_the_staged_gate_reviews_the_whole_commit_and_names_files_from_there() {
for relative in ["false", "true"] {
let dir = TempDir::new().expect("temp dir");
let root = dir.path();
common::git_init(root);
common::git_must(root, &["config", "--local", "diff.relative", relative]);
std::fs::create_dir(root.join("sub")).expect("subdirectory");
std::fs::write(root.join("README.md"), CLEAN_README).expect("write the README");
std::fs::write(root.join("GUIDE.md"), BROKEN_README).expect("write GUIDE.md");
std::fs::write(root.join("sub").join("README.md"), BROKEN_README)
.expect("write sub/README.md");
common::git_must(root, &["add", "README.md", "GUIDE.md", "sub/README.md"]);
let output = common::without_outer_git(env!("CARGO_BIN_EXE_drep"), &root.join("sub"))
.env("DREP_SITE_CONFIG", absent_site_policy())
.args(["lint-docs", "--staged", "--strict"])
.output()
.expect("drep must run");
let said = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(1),
"diff.relative={relative}: {said}"
);
for named in ["./README.md:5:2", "./../GUIDE.md:5:2"] {
assert!(
said.contains(named),
"diff.relative={relative}: {named} in {said}"
);
}
assert!(
!said.contains("./../README.md"),
"diff.relative={relative}: {said}"
);
}
}