use std::path::{Path, PathBuf};
use super::deterministic_staged::ruff_fixture;
use crate::analysis::result::FailureReason;
use crate::cli::OutputFormat;
use crate::cli::check::input::{PreCommitPush, resolve, resolve_pre_commit};
use crate::cli::check::{CheckArgs, deterministic};
use crate::test_support::{git_commit_all, git_init, git_output};
fn branch_args(base: &str, tip: Option<&str>) -> CheckArgs {
CheckArgs {
paths: Vec::new(),
staged: false,
diff: Some(base.to_owned()),
tip: tip.map(str::to_owned),
pre_commit_push: false,
format: OutputFormat::Text,
fail_on: None,
cache_only: false,
push_gate: false,
max_review_rounds: None,
unlimited_reviews: false,
}
}
fn branch_fixture(root: &Path) -> (PathBuf, String) {
git_init(root);
let argv = ruff_fixture(root);
std::fs::write(root.join(".gitignore"), "venv/\nargv.txt\n").expect(".gitignore");
std::fs::write(root.join("a.py"), "a = 1\n").expect("a.py");
git_commit_all(root, "base");
let base = git_output(root, &["rev-parse", "HEAD"]);
git_output(root, &["checkout", "--quiet", "-b", "feature"]);
std::fs::write(root.join("a.py"), "a = 2\n").expect("feature a.py");
git_commit_all(root, "feature");
(argv, base)
}
fn assert_refused(failures: &std::collections::BTreeMap<PathBuf, FailureReason>, file: &Path) {
match failures.get(file) {
Some(FailureReason::UncommittedChanges { tool, paths }) => {
assert_eq!(tool, "ruff");
assert_eq!(paths, &[PathBuf::from("a.py")]);
}
other => panic!("expected UncommittedChanges for {file:?}, got {other:?}"),
}
}
#[tokio::test]
async fn a_pushed_tip_that_is_not_checked_out_does_not_lint_the_working_tree() {
let dir = tempfile::tempdir().expect("tempdir");
let (argv, base) = branch_fixture(dir.path());
let tip = git_output(dir.path(), &["rev-parse", "feature"]);
git_output(dir.path(), &["checkout", "--quiet", "main"]);
let work = resolve(&branch_args(&base, Some(&tip)), dir.path(), false)
.await
.expect("branch diff resolves");
let (findings, failures, _compiled) = deterministic::run(&work, dir.path()).await;
assert!(
!argv.exists(),
"ruff must not lint a working tree that does not hold the pushed tip"
);
assert!(findings.is_empty(), "got {findings:?}");
assert_refused(&failures, Path::new("a.py"));
}
#[tokio::test]
async fn an_uncommitted_edit_refuses_a_linter_in_branch_mode() {
let dir = tempfile::tempdir().expect("tempdir");
let (argv, base) = branch_fixture(dir.path());
std::fs::write(dir.path().join("a.py"), "a = 3\n").expect("dirty a.py");
let work = resolve(&branch_args(&base, None), dir.path(), false)
.await
.expect("branch diff resolves");
let (_findings, failures, _compiled) = deterministic::run(&work, dir.path()).await;
assert!(!argv.exists(), "ruff must not lint an uncommitted edit");
assert_refused(&failures, Path::new("a.py"));
}
#[tokio::test]
async fn a_pre_commit_push_range_does_not_lint_a_working_tree_without_its_tip() {
let dir = tempfile::tempdir().expect("tempdir");
let (argv, base) = branch_fixture(dir.path());
let tip = git_output(dir.path(), &["rev-parse", "feature"]);
git_output(dir.path(), &["checkout", "--quiet", "main"]);
let range = PreCommitPush::Range {
from: base,
to: tip,
};
let work = resolve_pre_commit(dir.path(), &range, false)
.await
.expect("pre-commit range resolves");
let (_findings, failures, _compiled) = deterministic::run(&work, dir.path()).await;
assert!(!argv.exists(), "ruff must not lint a tree without the tip");
assert_refused(&failures, Path::new("a.py"));
}
#[tokio::test]
async fn a_clean_checkout_of_the_tip_runs_the_linter() {
let dir = tempfile::tempdir().expect("tempdir");
let (argv, base) = branch_fixture(dir.path());
let tip = git_output(dir.path(), &["rev-parse", "HEAD"]);
let work = resolve(&branch_args(&base, Some(&tip)), dir.path(), false)
.await
.expect("branch diff resolves");
let (_findings, failures, _compiled) = deterministic::run(&work, dir.path()).await;
assert!(failures.is_empty(), "got {failures:?}");
let runs = std::fs::read_to_string(&argv).expect("ruff ran");
assert_eq!(runs.lines().count(), 1, "ruff runs once: {runs}");
}