use super::support::{
Fixture, Observed, bounded, bounded_for, executable, fixture, git, observe, porcelain, remove, repository,
};
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
const LINTER_VARIABLE: &str = "GIT_POLICY_NATIVE_TEST_LINTER";
const SHOT_URL: &str = "https://lfs.example.com/org/repo/de7030234493a8bea844dbe1d8676e68a2c1a4b014c721f0425a22b6df66faec/pkg/asset/shot.png";
const RELATIVE: &[u8] = b"# T\n\n\n";
fn real_linter() -> PathBuf {
let Some(path) = std::env::var_os(LINTER_VARIABLE) else {
panic!(
"{LINTER_VARIABLE} is unset: build package/linter/monochromatic-lint and name its executable in \
{LINTER_VARIABLE}; the gate image sets it to the linter it builds"
);
};
return PathBuf::from(path);
}
fn path_with_linter(fixture: &Fixture, program: &Path) -> OsString {
let directory: PathBuf = fixture.root.join("linter");
std::fs::create_dir_all(&directory).expect("linter directory");
std::os::unix::fs::symlink(program, directory.join("monochromatic-lint")).expect("link");
let mut path: OsString = fixture.root.join("bin").into_os_string();
path.push(":");
path.push(directory);
path.push(":/usr/bin:/bin");
return path;
}
fn run_with(fixture: &Fixture, path: &OsStr, repo: &Path, arguments: &[&str]) -> Observed {
return observe(
bounded(fixture, fixture.root.join("bin/git").as_path(), path)
.current_dir(repo)
.args(arguments),
b"",
);
}
fn lfs_repository(fixture: &Fixture, setting: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
std::fs::create_dir_all(repo.join("pkg/asset")).expect("tree");
std::fs::write(
repo.join(".lfsconfig"),
b"[lfs]\n\turl = https://lfs.example.com/org/repo\n",
)
.expect("lfsconfig");
std::fs::write(
repo.join(".gitattributes"),
b"*.png filter=lfs diff=lfs merge=lfs -text\n",
)
.expect("attributes");
std::fs::write(repo.join("pkg/asset/shot.png"), b"image bytes").expect("image");
std::fs::write(
repo.join("cli-git.config.jsonc"),
format!(
"{{ \"policies\": {{ \"final-newline\": \"off\", \"markdown/autofix\": {setting} }} }}\n"
),
)
.expect("configuration");
git(fixture, repo.as_path(), &["add", "--all"]);
git(
fixture,
repo.as_path(),
&["commit", "--quiet", "--message=base"],
);
return repo;
}
fn autofix(sequence: u64, trigger: &str, severity: &str, path: &str, fix: &str) -> String {
return format!(
"{{\"schemaVersion\":1,\"sequence\":{sequence},\"type\":\"finding\",\"trigger\":\"{trigger}\",\
\"policyId\":\"markdown/autofix\",\"severity\":\"{severity}\",\"code\":\"markdown/autofix/markdown-autofix\",\
\"message\":\"monochromatic-lint --fix (markdown/lfs-image-url) rewrites {path}.\",\"path\":\"{path}\",\"fix\":\"{fix}\"}}\n"
);
}
fn staged(fixture: &Fixture, repo: &Path, path: &str) -> Vec<u8> {
return git(fixture, repo, &["show", format!(":{path}").as_str()]).stdout;
}
#[test]
fn the_real_linter_rewrites_through_add_check_and_fix() {
let fixture: Fixture = fixture("markdown-real");
let path: OsString = path_with_linter(&fixture, real_linter().as_path());
let repo: PathBuf = lfs_repository(&fixture, "[\"warn\", { \"exclude\": [\"package/ssg/\"] }]");
std::fs::write(repo.join("pkg/README.md"), RELATIVE).expect("relative link");
std::fs::create_dir_all(repo.join("package/ssg")).expect("excluded tree");
std::fs::write(
repo.join("package/ssg/a.md"),
b"\n",
)
.expect("excluded");
std::fs::write(
repo.join("pkg/done.md"),
format!("\n").as_bytes(),
)
.expect("already rewritten");
let added: Observed = run_with(&fixture, &path, &repo, &["add", "--", "pkg/README.md"]);
assert_eq!(
(
added.code,
String::from_utf8_lossy(&added.stderr).into_owned()
),
(
Some(0),
autofix(0, "pre-forward", "warn", "pkg/README.md", "none")
)
);
assert_eq!(staged(&fixture, repo.as_path(), "pkg/README.md"), RELATIVE);
let quiet: Observed = run_with(
&fixture,
&path,
&repo,
&["add", "--", "package/ssg/a.md", "pkg/done.md"],
);
assert_eq!((quiet.code, quiet.stderr), (Some(0), Vec::new()));
let checked: Observed = run_with(&fixture, &path, &repo, &["cli-git", "check", "--all"]);
assert_eq!(
(
checked.code,
String::from_utf8_lossy(&checked.stdout).into_owned()
),
(
Some(0),
autofix(0, "direct-check", "warn", "pkg/README.md", "none")
)
);
let index_before: Vec<u8> = std::fs::read(repo.join(".git/index")).expect("index");
let fixed: Observed = run_with(&fixture, &path, &repo, &["cli-git", "fix", "--all"]);
assert_eq!(
(
fixed.code,
String::from_utf8_lossy(&fixed.stdout).into_owned()
),
(
Some(0),
String::from(
"{\"schemaVersion\":1,\"sequence\":0,\"type\":\"fix-summary\",\"trigger\":\"direct-fix\",\
\"passes\":1,\"changedPaths\":[\"pkg/README.md\"]}\n"
)
)
);
assert_eq!(
std::fs::read(repo.join("pkg/README.md")).expect("rewritten"),
format!("# T\n\n\n").into_bytes()
);
assert_eq!(
std::fs::read(repo.join(".git/index")).expect("index"),
index_before
);
assert_eq!(staged(&fixture, repo.as_path(), "pkg/README.md"), RELATIVE);
std::fs::write(
repo.join("cli-git.config.jsonc"),
"{ \"policies\": { \"final-newline\": \"off\", \"markdown/autofix\": \"error\" } }\n",
)
.expect("configuration");
std::fs::write(repo.join("pkg/new.md"), RELATIVE).expect("new file");
let stopped: Observed = run_with(&fixture, &path, &repo, &["add", "--", "pkg/new.md"]);
assert_eq!(
(
stopped.code,
String::from_utf8_lossy(&stopped.stderr).into_owned()
),
(
Some(1),
autofix(0, "pre-forward", "error", "pkg/new.md", "none")
)
);
assert!(
porcelain(&fixture, repo.as_path()).contains("?? pkg/new.md\n"),
"{}",
porcelain(&fixture, repo.as_path())
);
remove(&fixture);
}
fn unchecked(trigger: &str, path: &str, reason: &str) -> String {
return format!(
"{{\"schemaVersion\":1,\"sequence\":0,\"type\":\"engine-failure\",\"code\":\"policy-incomplete\",\
\"message\":\"{reason}. The Markdown candidate was not checked and nothing was changed; fix the cause, \
or pass --no-enforce-markdown/autofix to skip this policy for one command.\",\
\"trigger\":\"{trigger}\",\"policyId\":\"markdown/autofix\",\"path\":\"{path}\"}}\n"
);
}
#[test]
fn a_missing_linter_stops_only_markdown_work() {
let fixture: Fixture = fixture("markdown-missing");
let repo: PathBuf = lfs_repository(&fixture, "[\"warn\", { \"exclude\": [\"skip/\"] }]");
let mut path: OsString = fixture.root.join("bin").into_os_string();
path.push(":/usr/bin:/bin");
std::fs::write(repo.join("a.md"), RELATIVE).expect("markdown");
std::fs::write(repo.join("b.txt"), b"text\n").expect("text");
let stopped: Observed = run_with(&fixture, &path, &repo, &["add", "--", "a.md"]);
assert_eq!(
(
stopped.code,
String::from_utf8_lossy(&stopped.stderr).into_owned()
),
(
Some(2),
unchecked(
"pre-forward",
"a.md",
"cli-git could not find monochromatic-lint in any absolute directory on PATH, and the policy \
markdown/autofix runs it for every Markdown candidate; install it so that it is on PATH"
)
)
);
let checked: Observed = run_with(&fixture, &path, &repo, &["cli-git", "check", "--", "a.md"]);
assert_eq!(checked.code, Some(2));
assert_eq!(
String::from_utf8_lossy(&checked.stdout).into_owned(),
unchecked(
"direct-check",
"a.md",
"cli-git could not find monochromatic-lint in any absolute directory on PATH, and the policy \
markdown/autofix runs it for every Markdown candidate; install it so that it is on PATH"
)
);
let text: Observed = run_with(&fixture, &path, &repo, &["add", "--", "b.txt"]);
assert_eq!((text.code, text.stderr), (Some(0), Vec::new()));
std::fs::create_dir(repo.join("skip")).expect("excluded directory");
std::fs::write(repo.join("skip/c.md"), RELATIVE).expect("excluded markdown");
let excluded: Observed = run_with(&fixture, &path, &repo, &["add", "--", "skip/c.md"]);
assert_eq!((excluded.code, excluded.stderr), (Some(0), Vec::new()));
let escaped: Observed = run_with(
&fixture,
&path,
&repo,
&["add", "--no-enforce-markdown/autofix", "--", "a.md"],
);
assert_eq!((escaped.code, escaped.stderr), (Some(0), Vec::new()));
assert_eq!(
porcelain(&fixture, repo.as_path()),
"A a.md\nA b.txt\nA skip/c.md\n"
);
remove(&fixture);
}
#[test]
fn a_failing_linter_stops_the_command() {
let fixture: Fixture = fixture("markdown-failing");
let program: PathBuf = fixture.root.join("failing-linter");
executable(
program.as_path(),
b"#!/bin/sh\necho 'monochromatic-lint: broken on purpose' >&2\nexit 3\n",
);
let path: OsString = path_with_linter(&fixture, program.as_path());
let repo: PathBuf = lfs_repository(&fixture, "\"warn\"");
std::fs::write(repo.join("a.md"), RELATIVE).expect("markdown");
let reason: &str = "monochromatic-lint exited with status 3, which its one-rule configuration never produces: broken on purpose";
let stopped: Observed = run_with(&fixture, &path, &repo, &["add", "--", "a.md"]);
assert_eq!(
(
stopped.code,
String::from_utf8_lossy(&stopped.stderr).into_owned()
),
(Some(2), unchecked("pre-forward", "a.md", reason))
);
let fixed: Observed = run_with(&fixture, &path, &repo, &["cli-git", "fix", "--all"]);
assert_eq!(
(
fixed.code,
String::from_utf8_lossy(&fixed.stdout).into_owned()
),
(Some(2), unchecked("direct-fix", "a.md", reason))
);
assert_eq!(
std::fs::read(repo.join("a.md")).expect("unchanged"),
RELATIVE
);
assert_eq!(porcelain(&fixture, repo.as_path()), "?? a.md\n");
remove(&fixture);
}
const COMMIT_BOUND_SECONDS: &str = "120";
fn head(fixture: &Fixture, repo: &Path) -> String {
return String::from_utf8_lossy(&git(fixture, repo, &["rev-parse", "HEAD"]).stdout)
.trim()
.to_owned();
}
#[test]
fn the_post_commit_stage_finds_the_linter_on_the_invocations_path() {
let fixture: Fixture = fixture("markdown-post-commit");
let log: PathBuf = fixture.root.join("linter-runs.log");
let program: PathBuf = fixture.root.join("recording-linter");
executable(
program.as_path(),
format!("#!/bin/sh\ncat .git/refs/heads/main >> '{}'\ncat\n", log.display()).as_bytes(),
);
let path: OsString = path_with_linter(&fixture, program.as_path());
let repo: PathBuf = lfs_repository(&fixture, "\"warn\"");
let base: String = head(&fixture, repo.as_path());
std::fs::write(repo.join("doc.md"), b"# T\n\nplain\n").expect("markdown");
git(&fixture, repo.as_path(), &["add", "--", "doc.md"]);
let committed: Observed = observe(
bounded_for(
&fixture,
fixture.root.join("bin/git").as_path(),
path.as_os_str(),
COMMIT_BOUND_SECONDS,
)
.current_dir(&repo)
.args(["commit", "--quiet", "--message=doc", "--", "doc.md"]),
b"",
);
let landed: String = head(&fixture, repo.as_path());
let runs: String = std::fs::read_to_string(&log).expect("linter runs");
assert_eq!(
(committed.code, String::from_utf8_lossy(&committed.stderr).into_owned()),
(Some(0), String::new()),
"runs: {runs}"
);
assert_ne!(landed, base);
assert_eq!(runs.lines().last(), Some(landed.as_str()), "runs: {runs}");
remove(&fixture);
}