utils/dep_freshness/tests/test_install.py
Krzysztof kuhy Rudnicki 71a43d9658
Some checks failed
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
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
design_system tests / test (push) Has been cancelled
sync_settings_ui tests / test (push) Has been cancelled
Script the gate rollout instead of hand-copying it into sixteen repos
The gate is three files per repo -- a delegate, a pre-commit hook entry and a
CI workflow -- and Phase 3 needs them in sixteen more repos. Hand-replication
is precisely how the 250-line cap ended up shipping under four different hook
ids, two of which were independent reimplementations rather than delegates, so
the copy is now a script and the templates have exactly one home.

  scripts/install_dependency_freshness_gate.sh <repo> [--check] [--fvm]

Idempotent, and it rewrites a copy that has drifted from the template rather
than leaving it. `--check` reports without writing. `--fvm` pins the Flutter
SDK, and only in a repo that actually holds a pubspec.yaml -- an .fvmrc in a
Python repo declares a toolchain nothing there builds with. An unresolvable
SDK version leaves the pin alone and exits 1: guessing would pin every repo to
a number nobody chose.

The pre-commit block is APPENDED rather than parsed and re-emitted. Every one
of these configs carries comments explaining why its hooks exist, and a YAML
round-trip through the standard library drops all of them.

306 tests, 100% branch coverage on every production module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
2026-08-28 14:12:00 +02:00

183 lines
6.5 KiB
Python

"""The gate installer: three pieces in, idempotent, and no comments lost.
The point of this module is that sixteen repos get byte-identical copies, so
the assertions compare against the templates rather than against a paraphrase
of them -- a test that spelled the expected content out by hand would let the
template and the installed file drift, which is the failure the installer
exists to prevent.
"""
from __future__ import annotations
import runpy
from pathlib import Path
import pytest
from dep_freshness import install as installer
from dep_freshness.install_cli import main
from dep_freshness.tests.conftest import write
TEMPLATES = Path(installer.__file__).resolve().parent / "templates"
def _template(name: str) -> str:
return (TEMPLATES / name).read_text(encoding="utf-8")
def test_a_bare_repo_gets_all_three_pieces(repo):
assert installer.install(repo) == [
"scripts/check_dependency_freshness.sh",
".github/workflows/dependency-freshness.yml",
".pre-commit-config.yaml (dependency-freshness hook)",
]
delegate = repo / "scripts/check_dependency_freshness.sh"
assert delegate.read_text(encoding="utf-8") == _template("delegate.sh")
assert delegate.stat().st_mode & 0o111, "the hook execs it; it must be +x"
assert (repo / ".github/workflows/dependency-freshness.yml").read_text(
encoding="utf-8"
) == _template("workflow.yml")
assert "id: dependency-freshness" in (repo / ".pre-commit-config.yaml").read_text(
encoding="utf-8"
)
def test_a_second_run_writes_nothing(repo):
installer.install(repo)
assert installer.install(repo) == []
assert installer.plan(repo) == []
def test_plan_names_what_is_missing_without_writing_it(repo):
assert installer.plan(repo) == [
"scripts/check_dependency_freshness.sh",
".github/workflows/dependency-freshness.yml",
".pre-commit-config.yaml (dependency-freshness hook)",
]
assert not (repo / "scripts").exists()
def test_a_drifted_copy_is_rewritten(repo):
installer.install(repo)
delegate = repo / "scripts/check_dependency_freshness.sh"
delegate.write_text("#!/bin/bash\necho lol\n", encoding="utf-8")
workflow = repo / ".github/workflows/dependency-freshness.yml"
workflow.write_text("name: something else\n", encoding="utf-8")
assert installer.install(repo) == [
"scripts/check_dependency_freshness.sh",
".github/workflows/dependency-freshness.yml",
]
assert delegate.read_text(encoding="utf-8") == _template("delegate.sh")
def test_an_existing_pre_commit_config_keeps_its_hooks_and_comments(repo):
"""Appending, not re-emitting: a YAML round-trip drops every comment.
Each of these configs explains why its hooks exist, and that prose is the
only record of it.
"""
write(
repo,
".pre-commit-config.yaml",
"""\
# The 250-line cap. A file that cannot be read in one piece forces re-reads.
repos:
- repo: local
hooks:
- id: file-length-cap
name: file length <= 250 lines
entry: bash scripts/check_file_length.sh
language: system
""",
)
installer.install(repo)
body = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
assert "# The 250-line cap." in body
assert "id: file-length-cap" in body
assert "id: dependency-freshness" in body
def test_a_config_that_already_has_the_hook_is_left_alone(repo):
write(
repo,
".pre-commit-config.yaml",
"""\
repos:
- repo: local
hooks:
- id: dependency-freshness
entry: scripts/check_dependency_freshness.sh
language: system
""",
)
before = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
installer.install(repo)
assert (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8") == before
def test_a_config_without_a_trailing_newline_still_appends_cleanly(repo):
write(repo, ".pre-commit-config.yaml", "repos:\n - repo: local\n hooks:")
installer.install(repo)
body = (repo / ".pre-commit-config.yaml").read_text(encoding="utf-8")
assert body.startswith("repos:\n - repo: local\n hooks:\n\n #")
assert "id: dependency-freshness" in body
def test_fvmrc_is_only_for_repos_that_hold_a_pubspec(repo):
assert installer.write_fvmrc(repo, "3.47.2") is False
assert not (repo / ".fvmrc").exists()
write(repo, "app/pubspec.yaml", "name: demo\n")
assert installer.write_fvmrc(repo, "3.47.2") is True
assert (repo / ".fvmrc").read_text(
encoding="utf-8"
) == '{\n "flutter": "3.47.2"\n}\n'
assert installer.write_fvmrc(repo, "3.47.2") is False
def test_the_cli_installs_and_reports_each_piece(repo, capsys):
assert main([str(repo)]) == 0
out = capsys.readouterr().out
assert "wrote: scripts/check_dependency_freshness.sh" in out
assert "wrote: .github/workflows/dependency-freshness.yml" in out
def test_the_cli_check_mode_writes_nothing(repo, capsys):
assert main([str(repo), "--check"]) == 0
assert (
"would write: scripts/check_dependency_freshness.sh" in capsys.readouterr().out
)
assert not (repo / "scripts").exists()
main([str(repo)])
assert main([str(repo), "--check"]) == 0
assert "gate already current" in capsys.readouterr().out
def test_the_cli_pins_the_sdk_when_asked(repo, monkeypatch, capsys):
monkeypatch.setattr(
"dep_freshness.install_cli.flutter_latest", lambda: ("3.47.2", "3.13.2")
)
write(repo, "pubspec.yaml", "name: demo\n")
assert main([str(repo), "--fvm"]) == 0
assert "wrote: .fvmrc (flutter 3.47.2)" in capsys.readouterr().out
# Same version again: the pin is already right, so nothing is reported.
assert main([str(repo), "--fvm"]) == 0
assert ".fvmrc" not in capsys.readouterr().out
def test_an_unresolvable_sdk_leaves_the_pin_alone(repo, monkeypatch, capsys):
"""Offline, the alternative is pinning every repo to a guessed number."""
monkeypatch.setattr(
"dep_freshness.install_cli.flutter_latest", lambda: (None, None)
)
write(repo, "pubspec.yaml", "name: demo\n")
assert main([str(repo), "--fvm"]) == 1
assert "could not resolve latest stable Flutter" in capsys.readouterr().err
assert not (repo / ".fvmrc").exists()
def test_the_module_entrypoint_runs_the_cli(repo, monkeypatch):
monkeypatch.setattr("sys.argv", ["install_main", str(repo), "--check"])
with pytest.raises(SystemExit) as exit_info:
runpy.run_module("dep_freshness.install_main", run_name="__main__")
assert exit_info.value.code == 0