use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
fn repo_root() -> PathBuf {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
crate_dir
.parent()
.expect("crate directory should have a parent")
.parent()
.expect("crates directory should have a parent repo root")
.to_path_buf()
}
fn write_file(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent directories");
}
std::fs::write(path, content).unwrap_or_else(|err| panic!("write {}: {err}", path.display()));
}
fn write_lines(path: &Path, count: usize) {
let mut body = String::new();
for index in 0..count {
body.push_str(&format!("line {index}\n"));
}
write_file(path, &body);
}
fn copy_repo_file(repo_root: &Path, temp_repo: &Path, relative_path: &str) {
let source = repo_root.join(relative_path);
let content = std::fs::read_to_string(&source)
.unwrap_or_else(|err| panic!("read {}: {err}", source.display()));
write_file(&temp_repo.join(relative_path), &content);
}
fn git(temp_repo: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(temp_repo)
.output()
.expect("run git");
assert!(
output.status.success(),
"git {:?} failed\nstdout:\n{}\nstderr:\n{}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn init_temp_repo() -> TempDir {
let root = repo_root();
let temp_repo = tempfile::tempdir().expect("create temp repo");
let repo_path = temp_repo.path();
copy_repo_file(&root, repo_path, "scripts/check-file-size-limits.sh");
copy_repo_file(&root, repo_path, "scripts/lib/file_size_limits.py");
write_file(&repo_path.join("README.md"), "# Temp repo\n");
git(repo_path, &["init", "-b", "main"]);
git(repo_path, &["config", "user.name", "Codex"]);
git(repo_path, &["config", "user.email", "codex@example.com"]);
git(repo_path, &["add", "."]);
git(repo_path, &["commit", "-m", "initial"]);
temp_repo
}
fn run_check_script(temp_repo: &Path, args: &[&str]) -> Output {
Command::new("bash")
.arg(temp_repo.join("scripts/check-file-size-limits.sh"))
.args(args)
.current_dir(temp_repo)
.output()
.expect("run check-file-size-limits.sh")
}
fn output_text(output: &Output) -> (String, String) {
(
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
#[test]
fn check_file_size_limits_help_lists_usage_and_exit_codes() {
let temp_repo = init_temp_repo();
let output = run_check_script(temp_repo.path(), &["--help"]);
let (stdout, stderr) = output_text(&output);
assert!(
output.status.success(),
"expected --help to succeed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(stdout.contains("Usage:"), "missing usage block\n{stdout}");
assert!(
stdout.contains("Exit codes:"),
"missing exit-codes block\n{stdout}"
);
}
#[test]
fn check_file_size_limits_passes_when_all_files_are_within_limits() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join("crates/cueloop/src/lib.rs"), 12);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(0),
"expected success status\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("OK: file-size limits within policy"),
"expected success marker\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_warns_on_soft_limit_without_failing() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join("docs/guides/large.md"), 801);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(0),
"expected soft-limit warning to stay non-blocking\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("WARN: soft file-size limit exceeded:"),
"missing soft warning header\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("docs/guides/large.md"),
"expected offender path in soft warning\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_fails_on_hard_limit_violation() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join("crates/cueloop/src/huge.rs"), 1001);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(1),
"expected hard-limit failure\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("ERROR: hard file-size limit exceeded:"),
"missing hard-error header\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("crates/cueloop/src/huge.rs"),
"expected offender path in hard error\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_ignores_default_generated_path_excludes() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join("schemas/config.schema.json"), 1500);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(0),
"excluded schema path should not fail policy\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
!stdout.contains("schemas/config.schema.json"),
"excluded path should not be listed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_ignores_default_runtime_bookkeeping_excludes() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join(".cueloop/done.jsonc"), 1500);
write_lines(&repo_path.join(".cueloop/done.jsonc"), 1500);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(0),
"excluded runtime bookkeeping paths should not fail policy\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
!stdout.contains(".cueloop/done.jsonc"),
"excluded legacy bookkeeping path should not be listed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
!stdout.contains(".cueloop/done.jsonc"),
"excluded CueLoop bookkeeping path should not be listed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_includes_untracked_monitored_files() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
let untracked_path = repo_path.join("scratch/oversized.md");
write_lines(&untracked_path, 1001);
let git_status = Command::new("git")
.args(["status", "--porcelain"])
.current_dir(repo_path)
.output()
.expect("run git status --porcelain");
assert!(git_status.status.success(), "git status should succeed");
assert!(
String::from_utf8_lossy(&git_status.stdout).contains("?? scratch"),
"expected oversized file to remain untracked"
);
let output = run_check_script(repo_path, &[]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(1),
"untracked oversized markdown should fail\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("scratch/oversized.md"),
"expected untracked offender path in output\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_supports_configurable_exclude_glob() {
let temp_repo = init_temp_repo();
let repo_path = temp_repo.path();
write_lines(&repo_path.join("generated/manual-long.md"), 1001);
let output = run_check_script(repo_path, &["--exclude-glob", "generated/**"]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(0),
"custom exclude should suppress the generated path\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
!stdout.contains("generated/manual-long.md"),
"custom-excluded path should not appear\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[test]
fn check_file_size_limits_invalid_arg_exits_with_usage_error() {
let temp_repo = init_temp_repo();
let output = run_check_script(temp_repo.path(), &["--definitely-not-valid"]);
let (stdout, stderr) = output_text(&output);
assert_eq!(
output.status.code(),
Some(2),
"invalid arguments should return usage exit code\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let combined = format!("{stdout}\n{stderr}").to_lowercase();
assert!(
combined.contains("usage:"),
"expected usage text for invalid args\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}