use std::path::PathBuf;
use crate::analysis::result::FailureReason;
use crate::cli::OutputFormat;
use crate::cli::check::CheckArgs;
use crate::cli::check::input::READ_MAX_BYTES;
use crate::cli::check::input::resolve;
fn paths_args(paths: Vec<PathBuf>) -> CheckArgs {
CheckArgs {
paths,
staged: false,
diff: None,
tip: None,
format: OutputFormat::Text,
fail_on: None,
cache_only: false,
push_gate: false,
}
}
fn diff_args(ref_: &str) -> CheckArgs {
CheckArgs {
paths: Vec::new(),
staged: false,
diff: Some(ref_.to_owned()),
tip: None,
format: OutputFormat::Text,
fail_on: None,
cache_only: false,
push_gate: false,
}
}
#[tokio::test]
async fn paths_mode_yields_one_whole_file_hunk_covering_every_line() {
let dir = tempfile::tempdir().expect("tempdir");
let body = "a = 1\nb = 2\nc = 3\nd = 4\ne = 5\n";
std::fs::write(dir.path().join("lib.py"), body).expect("write lib.py");
let args = paths_args(vec![dir.path().join("lib.py")]);
let work = resolve(&args, dir.path()).await.expect("paths resolve");
assert!(
work.read_failures.is_empty(),
"a readable .py file must not land in read_failures, got {:?}",
work.read_failures
);
assert_eq!(
work.by_file.len(),
1,
"one file must yield one entry in by_file"
);
let hunks = &work.by_file[0];
assert_eq!(
hunks.len(),
1,
"whole-file mode must produce exactly one hunk per file"
);
let hunk = &hunks[0];
assert_eq!(hunk.file_path, dir.path().join("lib.py"));
assert_eq!(
hunk.lines.len(),
5,
"every line of the file must appear in the hunk (got {})",
hunk.lines.len()
);
assert_eq!(hunk.new_start, 1, "whole-file hunk starts at line 1");
assert_eq!(
hunk.new_count, 5,
"whole-file hunk declares a line count matching its content"
);
}
#[tokio::test]
async fn oversized_file_fails_is_not_in_by_file_and_is_still_linted() {
let dir = tempfile::tempdir().expect("tempdir");
let size = usize::try_from(READ_MAX_BYTES + 1).expect("read limit fits in usize");
std::fs::write(dir.path().join("big.py"), vec![b'x'; size]).expect("write big.py");
let args = paths_args(vec![dir.path().join("big.py")]);
let work = resolve(&args, dir.path()).await.expect("paths resolve");
assert!(
work.by_file.is_empty(),
"an oversize file must not enter by_file, got {} entries",
work.by_file.len()
);
assert_eq!(
work.lint_only,
vec![dir.path().join("big.py")],
"too large for the model is not too large for ruff; the path must \
still reach the deterministic layer"
);
let reason = work
.read_failures
.get(&dir.path().join("big.py"))
.expect("oversize file must appear in read_failures");
match reason {
FailureReason::FileTooLarge { bytes, limit } => {
assert_eq!(*bytes, u64::try_from(size).expect("file size fits in u64"));
assert_eq!(*limit, READ_MAX_BYTES);
}
other => panic!("expected FileTooLarge, got {other:?}"),
}
}
#[tokio::test]
async fn non_utf8_file_lands_in_failures_as_unreadable() {
let dir = tempfile::tempdir().expect("tempdir");
let bytes = [0xFFu8, 0xFE, 0xFD, 0x80, 0x81];
std::fs::write(dir.path().join("bad.py"), bytes).expect("write bad.py");
let args = paths_args(vec![dir.path().join("bad.py")]);
let work = resolve(&args, dir.path()).await.expect("paths resolve");
assert!(
work.by_file.is_empty(),
"a non-UTF-8 file must not enter by_file"
);
let reason = work
.read_failures
.get(&dir.path().join("bad.py"))
.expect("non-UTF-8 file must appear in read_failures");
assert!(
matches!(reason, FailureReason::Unreadable(_)),
"expected Unreadable, got {reason:?}"
);
}
#[tokio::test]
async fn diff_against_nonexistent_ref_returns_err() {
let dir = tempfile::tempdir().expect("tempdir");
init_repo_with_commit(dir.path());
let args = diff_args("definitely-not-a-real-ref-xyz");
let result = resolve(&args, dir.path()).await;
assert!(
result.is_err(),
"an unknown ref must return Err, got Ok({:?})",
result.map(|w| (w.by_file.len(), w.read_failures.len()))
);
}
#[tokio::test]
async fn diff_ref_starting_with_dash_is_rejected_before_git() {
let dir = tempfile::tempdir().expect("tempdir");
let args = diff_args("--output=/tmp/whatever");
let result = resolve(&args, dir.path()).await;
let err = match result {
Ok(_) => panic!("a dash-prefixed ref must be rejected, got Ok(_)"),
Err(e) => e,
};
let msg = format!("{err:#}");
assert!(
msg.contains("--output=/tmp/whatever"),
"error must name the rejected ref, got {msg:?}"
);
}
fn init_repo_with_commit(root: &std::path::Path) {
use std::process::Command;
let run = |args: &[&str]| {
let out = Command::new("git")
.args(args)
.current_dir(root)
.output()
.expect("git spawns");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
};
run(&["init", "--quiet"]);
run(&["config", "user.email", "test@example.com"]);
run(&["config", "user.name", "test"]);
std::fs::write(root.join("seed.txt"), "seed\n").expect("seed");
run(&["add", "seed.txt"]);
run(&[
"-c",
"commit.gpgsign=false",
"commit",
"--quiet",
"--no-verify",
"-m",
"init",
]);
}
#[tokio::test]
async fn a_file_exactly_at_the_size_limit_is_accepted_and_one_byte_over_is_not() {
let dir = tempfile::tempdir().expect("tempdir");
let limit = usize::try_from(READ_MAX_BYTES).expect("read limit fits in usize");
let exact = dir.path().join("exact.py");
std::fs::write(&exact, "a".repeat(limit)).expect("write exact");
let over = dir.path().join("over.py");
std::fs::write(&over, "a".repeat(limit + 1)).expect("write over");
let args = paths_args(vec![exact.clone(), over.clone()]);
let work = crate::cli::check::input::resolve(&args, dir.path())
.await
.expect("resolve");
assert!(
!work.read_failures.contains_key(&exact),
"a file exactly at the limit must be accepted, failures: {:?}",
work.read_failures.keys().collect::<Vec<_>>()
);
assert!(
work.read_failures.contains_key(&over),
"one byte over the limit must fail, failures: {:?}",
work.read_failures.keys().collect::<Vec<_>>()
);
}
#[tokio::test]
async fn bare_check_with_no_paths_expands_the_root_instead_of_reading_a_directory() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("one.py"), "a = 1\n").expect("one.py");
std::fs::create_dir_all(dir.path().join("pkg")).expect("mkdir");
std::fs::write(dir.path().join("pkg/two.py"), "b = 2\n").expect("two.py");
let args = paths_args(Vec::new());
let work = resolve(&args, dir.path()).await.expect("bare resolve");
assert!(
work.read_failures.is_empty(),
"the root directory must be walked, not read as a file, got {:?}",
work.read_failures
);
assert_eq!(
work.by_file.len(),
2,
"both .py files under root must be analyzed, got {:?}",
work.by_file
.iter()
.filter_map(|h| h.first().map(|h| h.file_path.clone()))
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn an_explicitly_named_missing_path_is_a_failure_not_a_clean_run() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("typo.py");
let args = paths_args(vec![missing.clone()]);
let work = resolve(&args, dir.path()).await.expect("resolve");
assert!(
work.by_file.is_empty(),
"nothing to analyze, got {:?}",
work.by_file.len()
);
assert!(
work.read_failures.contains_key(&missing),
"a named path that does not exist must reach the gate as a failure, got {:?}",
work.read_failures
);
}
#[tokio::test]
async fn an_unreadable_file_reports_one_prefix_not_two() {
let dir = tempfile::tempdir().expect("tempdir");
let bad = dir.path().join("bad.py");
std::fs::write(&bad, [0xFFu8, 0xFE, 0xFD]).expect("write bad bytes");
let args = paths_args(vec![bad.clone()]);
let work = resolve(&args, dir.path()).await.expect("resolve");
let reason = work.read_failures.get(&bad).expect("bad.py must fail");
let line = reason.to_string();
assert_eq!(
line.matches("could not").count(),
1,
"the reason must read as one sentence, got: {line}"
);
}