mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 22:47:14 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
A fork of a third-party project can carry data that is code only by extension (TachiyomiSY's 6000-line exh/eh/tags/*.kt string tables). The reason belongs in that repo next to the data, so the gate now reads a .file-length-exempt of '<glob> # <reason>' lines from its cwd. A glob without a reason is a suppression and exits 2 instead of passing. The package had no tests at all; it now has a suite at 100% line+branch, wired into shared-gates-tests.yml with --cov-fail-under=100. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Yc4TiPjhbQLfH35QBPfrbP
152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
"""The gate entry point: counting, skipping, git-ignore, and exit codes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import runpy
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from file_length import check
|
|
from file_length.repo_exempt import EXEMPT_FILE
|
|
from file_length.tests.conftest import lines, write
|
|
|
|
|
|
def run_main(monkeypatch: pytest.MonkeyPatch, *argv: str) -> int:
|
|
monkeypatch.setattr(sys, "argv", ["check.py", *argv])
|
|
return check.main()
|
|
|
|
|
|
def test_count_lines_variants(tmp_path: Path) -> None:
|
|
assert check.count_lines(write(tmp_path, "a.py", lines(3))) == 3
|
|
assert check.count_lines(write(tmp_path, "b.py", "no newline")) == 1
|
|
assert check.count_lines(write(tmp_path, "c.py", "")) == 0
|
|
binary = tmp_path / "d.py"
|
|
binary.write_bytes(b"ab\x00cd\n")
|
|
assert check.count_lines(binary) is None
|
|
assert check.count_lines(tmp_path / "missing.py") is None
|
|
|
|
|
|
def test_count_lines_spans_chunks(tmp_path: Path) -> None:
|
|
big = tmp_path / "big.py"
|
|
big.write_bytes(b"x\n" * (1 << 20))
|
|
assert check.count_lines(big) == 1 << 20
|
|
|
|
|
|
def test_absolutize(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.chdir(tmp_path)
|
|
assert check.absolutize(Path("a.py")) == tmp_path / "a.py"
|
|
assert check.absolutize(tmp_path / "b.py") == tmp_path / "b.py"
|
|
|
|
|
|
def test_exempt_reasons(repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
assert check.exempt_reason(Path("x.html"), 1) == "not a capped extension"
|
|
assert check.exempt_reason(Path("node_modules/x.js"), 1) == "vendored / third-party"
|
|
write(repo, EXEMPT_FILE, "tags/*.kt # tag tables\n")
|
|
assert check.exempt_reason(Path("tags/A.kt"), 1) == "repo exemption: tag tables"
|
|
art = Path("docs/superpowers/plans/p.md")
|
|
assert check.exempt_reason(art, 1) == "frozen session artifact"
|
|
assert check.exempt_reason(Path("m.g.dart"), 1) == "generated"
|
|
words = write(repo, "w.txt", "a\n" * 10)
|
|
assert check.exempt_reason(words, 10) == "data-ish text (wordlist)"
|
|
code = write(repo, "c.py", lines(3))
|
|
assert check.exempt_reason(code, 3) is None
|
|
|
|
def boom(_self: Path) -> None:
|
|
raise OSError("gone")
|
|
|
|
monkeypatch.setattr(Path, "stat", boom)
|
|
assert check.exempt_reason(Path("c.py"), 3) == "unreadable"
|
|
|
|
|
|
def test_git_ignored(repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
write(repo, ".gitignore", "*.log\n")
|
|
kept = write(repo, "a.py", "x\n")
|
|
dropped = write(repo, "b.log", "x\n")
|
|
assert check.git_ignored([], repo) == set()
|
|
assert check.git_ignored([kept, dropped], repo) == {dropped}
|
|
assert check.git_ignored([kept], repo.parent / "not-a-repo-xyz") == set()
|
|
|
|
def no_git(*_a: object, **_k: object) -> None:
|
|
raise OSError("no git")
|
|
|
|
monkeypatch.setattr(subprocess, "run", no_git)
|
|
assert check.git_ignored([kept], repo) == set()
|
|
|
|
|
|
def test_iter_all_skips_excluded_ignored_and_symlinks(repo: Path) -> None:
|
|
write(repo, ".gitignore", "out/\n")
|
|
keep = write(repo, "src/a.py", "x\n")
|
|
write(repo, "node_modules/b.py", "x\n")
|
|
write(repo, "out/c.py", "x\n")
|
|
(repo / "link.py").symlink_to(keep)
|
|
found = set(check.iter_all(repo))
|
|
assert keep in found
|
|
assert not any(p.name in {"b.py", "c.py", "link.py"} for p in found)
|
|
|
|
|
|
def test_main_requires_paths_or_all(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
with pytest.raises(SystemExit) as exc:
|
|
run_main(monkeypatch)
|
|
assert exc.value.code == 2
|
|
|
|
|
|
def test_main_all_reports_violations_sorted(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
write(repo, "small.py", lines(10))
|
|
write(repo, "mid.py", lines(260))
|
|
write(repo, "huge.py", lines(400))
|
|
write(repo, "skip.g.dart", lines(400))
|
|
assert run_main(monkeypatch, "--all", "--explain") == 1
|
|
out, err = capsys.readouterr()
|
|
assert "skip" in out and "generated" in out
|
|
assert err.index("huge.py") < err.index("mid.py")
|
|
assert "small.py" not in err
|
|
|
|
|
|
def test_main_paths_honour_gitignore_and_missing(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
write(repo, ".gitignore", "*.log\n")
|
|
write(repo, "a.log", lines(300))
|
|
write(repo, "b.py", lines(300))
|
|
assert run_main(monkeypatch, "a.log", "missing.py") == 0
|
|
assert run_main(monkeypatch, "b.py") == 1
|
|
|
|
|
|
def test_main_skips_binary_and_honours_repo_exemption(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
(repo / "blob.py").write_bytes(b"\x00" * 10)
|
|
write(repo, "tags/Big.kt", lines(6000))
|
|
write(repo, EXEMPT_FILE, "tags/*.kt # tag data tables\n")
|
|
assert run_main(monkeypatch, "--all") == 0
|
|
|
|
|
|
def test_main_fails_closed_on_malformed_exempt_file(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
write(repo, EXEMPT_FILE, "tags/*.kt\n")
|
|
assert run_main(monkeypatch, "--all") == 2
|
|
assert "File-length gate ERROR" in capsys.readouterr().err
|
|
|
|
|
|
def test_main_tolerates_a_file_vanishing_mid_run(
|
|
repo: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(check, "iter_all", lambda _root: [repo / "gone.py"])
|
|
assert run_main(monkeypatch, "--all") == 0
|
|
|
|
|
|
def test_runs_as_a_plain_script(repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
write(repo, "ok.py", lines(3))
|
|
monkeypatch.setattr(sys, "argv", ["check.py", "--all"])
|
|
with pytest.raises(SystemExit) as exc:
|
|
runpy.run_path(str(Path(check.__file__)), run_name="__main__")
|
|
assert exc.value.code == 0
|