mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
[ci] Sync pre-commit revs and prek version from requirements files (#19026)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
parent
d34d3994e1
commit
5e37872da2
@@ -244,11 +244,20 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Read prek version from requirements_test.txt
|
||||
id: prek
|
||||
# requirements_test.txt is the only place the version is pinned, so a
|
||||
# Dependabot bump there is picked up here without a second edit.
|
||||
run: |
|
||||
if ! version=$(sed -nE 's/^prek==([^[:space:]#]+).*/\1/p' requirements_test.txt) || [ -z "$version" ]; then
|
||||
echo "::error::No prek== pin found in requirements_test.txt."
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
|
||||
with:
|
||||
# Keep in sync with requirements_test.txt.
|
||||
prek-version: "0.4.11"
|
||||
prek-version: ${{ steps.prek.outputs.version }}
|
||||
# This job only runs on pull requests, so nothing ever populates
|
||||
# the cache on dev. Every run would miss and then write a per-pull
|
||||
# request copy, which is what the old seed-cache job existed to
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Keeps pre-commit hook revs in sync with the requirements files.
|
||||
#
|
||||
# Dependabot only bumps the pins in requirements*.txt. Some of those tools
|
||||
# are pinned again as hook revs in .pre-commit-config.yaml. This workflow
|
||||
# runs script/sync_dependency_versions.py against the pull request branch
|
||||
# and pushes a commit with the revs updated.
|
||||
|
||||
name: Sync dependency versions
|
||||
|
||||
on:
|
||||
# pull_request_target rather than pull_request so the App secret is
|
||||
# available on Dependabot pull requests (pull_request runs opened by
|
||||
# Dependabot only see Dependabot secrets). The job below only touches
|
||||
# branches in this repository and only ever executes the script from the
|
||||
# base branch checkout, so fork code never runs with the token.
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- requirements_dev.txt
|
||||
- requirements_test.txt
|
||||
- .pre-commit-config.yaml
|
||||
- script/sync_dependency_versions.py
|
||||
|
||||
# The push to the pull request branch uses the App token minted below, so
|
||||
# the workflow's GITHUB_TOKEN does not need any scopes.
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync pinned versions
|
||||
runs-on: ubuntu-latest
|
||||
# Same-repository branches only: a push to a fork is not possible with
|
||||
# this token, and it keeps untrusted heads out of a privileged job.
|
||||
if: >-
|
||||
github.repository == 'esphome/esphome'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
# A push made with the workflow's own GITHUB_TOKEN would not start
|
||||
# CI on the new commit; a push with the App token does.
|
||||
permission-contents: write # git push of the sync commit to the pull request branch
|
||||
|
||||
- name: Check out base branch
|
||||
# Provides the script that runs below. Deliberately the base branch
|
||||
# so the pull request cannot change what executes here.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check out pull request branch
|
||||
# No allow-unsafe-pr-checkout here on purpose: checkout v7 only
|
||||
# refuses heads that live in a different repository, and the job
|
||||
# condition above already limits runs to same-repository branches.
|
||||
# Leaving it off keeps that refusal as a backstop for fork heads.
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
path: pull-request
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install yamlrocks
|
||||
# The script edits YAML through yamlrocks. Take the pin from the
|
||||
# base branch requirements so this workflow has no copy of its own.
|
||||
run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)"
|
||||
|
||||
- name: Sync pinned versions
|
||||
run: python script/sync_dependency_versions.py --root pull-request
|
||||
|
||||
- name: Push changes
|
||||
working-directory: pull-request
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "All pinned versions already match the requirements files."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "esphome[bot]"
|
||||
git config user.email "115708604+esphome[bot]@users.noreply.github.com"
|
||||
git commit -am "Sync pinned tool versions with requirements files"
|
||||
git push
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||
@@ -11,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.3
|
||||
rev: v0.16.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -42,7 +41,7 @@ repos:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
rev: v1.38.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||
|
||||
@@ -840,7 +840,7 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
cv.rename_key(
|
||||
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
|
||||
),
|
||||
cv.Schema({ ... }),
|
||||
cv.Schema({...}),
|
||||
)
|
||||
```
|
||||
For other deprecations, warn manually during validation:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Useful stuff when working in a development environment
|
||||
clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating
|
||||
clang-format==13.0.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
clang-tidy==22.1.8
|
||||
yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating
|
||||
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
pylint==4.0.8
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.16.5 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
prek==0.5.1 # also change in .github/workflows/ci.yml when updating
|
||||
flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
ruff==0.16.5 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
prek==0.5.1 # .github/workflows/ci.yml reads this pin
|
||||
yamlrocks==0.6.1 # used by script/sync_dependency_versions.py
|
||||
|
||||
# Unit tests
|
||||
pytest==9.1.1
|
||||
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep pre-commit hook revs in sync with the requirements files.
|
||||
|
||||
Dependabot only bumps the ``package==version`` pins in ``requirements*.txt``.
|
||||
Some of those tools are pinned a second time as hook ``rev`` values in
|
||||
``.pre-commit-config.yaml``. This script treats the requirements files as
|
||||
the source of truth and rewrites the revs to match, editing the config
|
||||
through yamlrocks so comments and layout survive.
|
||||
|
||||
Run without arguments to apply the changes in place, or with ``--check`` to
|
||||
only report drift (exit status 1 when anything is out of sync).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import yamlrocks
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
PRECOMMIT_CONFIG = ".pre-commit-config.yaml"
|
||||
|
||||
|
||||
class SyncError(Exception):
|
||||
"""A pin could not be located in a requirements file or the config."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SyncTarget:
|
||||
"""A requirements pin and the pre-commit repo whose rev mirrors it."""
|
||||
|
||||
package: str
|
||||
requirements_file: str
|
||||
repo: str
|
||||
|
||||
|
||||
SYNC_TARGETS: tuple[SyncTarget, ...] = (
|
||||
SyncTarget(
|
||||
"ruff", "requirements_test.txt", "https://github.com/astral-sh/ruff-pre-commit"
|
||||
),
|
||||
SyncTarget("flake8", "requirements_test.txt", "https://github.com/PyCQA/flake8"),
|
||||
SyncTarget(
|
||||
"pyupgrade", "requirements_test.txt", "https://github.com/asottile/pyupgrade"
|
||||
),
|
||||
SyncTarget(
|
||||
"clang-format",
|
||||
"requirements_dev.txt",
|
||||
"https://github.com/pre-commit/mirrors-clang-format",
|
||||
),
|
||||
SyncTarget(
|
||||
"yamllint",
|
||||
"requirements_dev.txt",
|
||||
"https://github.com/adrienverge/yamllint.git",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def read_requirement_version(requirements: str, package: str) -> str | None:
|
||||
"""Return the ``==`` pin for ``package`` or None when it is not pinned."""
|
||||
pattern = re.compile(
|
||||
rf"^{re.escape(package)}==(?P<version>[^\s#]+)",
|
||||
re.MULTILINE | re.IGNORECASE,
|
||||
)
|
||||
match = pattern.search(requirements)
|
||||
return match.group("version") if match else None
|
||||
|
||||
|
||||
def find_repo_entry(doc: Any, repo: str) -> Any:
|
||||
"""Return the single ``- repo:`` block for ``repo`` in a pre-commit doc."""
|
||||
try:
|
||||
entries = [entry for entry in doc["repos"] if entry["repo"] == repo]
|
||||
except KeyError as err:
|
||||
raise SyncError(f"malformed pre-commit config, missing key {err}") from None
|
||||
if len(entries) != 1:
|
||||
raise SyncError(
|
||||
f"expected exactly one block for repo {repo}, found {len(entries)}"
|
||||
)
|
||||
return entries[0]
|
||||
|
||||
|
||||
def current_rev(entry: Any, repo: str) -> tuple[str, str]:
|
||||
"""Split the block's rev into its tag prefix (``v`` or empty) and version."""
|
||||
if "rev" not in entry:
|
||||
raise SyncError(f"repo {repo} has no rev")
|
||||
rev = entry["rev"]
|
||||
if not isinstance(rev, str):
|
||||
# A rev such as ``1.0`` parses as a number and cannot be compared or
|
||||
# rewritten safely; quote it in the config instead.
|
||||
raise SyncError(f"rev of repo {repo} is not a string: {rev!r}")
|
||||
prefix = "v" if rev.startswith("v") else ""
|
||||
return prefix, rev.removeprefix("v")
|
||||
|
||||
|
||||
def sync(root: Path, *, write: bool) -> list[str]:
|
||||
"""Bring every hook rev in line with its requirements pin.
|
||||
|
||||
Returns one description per rev that was (or, when ``write`` is False,
|
||||
would be) changed. Raises SyncError when a pin cannot be found, which
|
||||
means SYNC_TARGETS has gone stale and needs updating by hand.
|
||||
"""
|
||||
config_path = root / PRECOMMIT_CONFIG
|
||||
doc = yamlrocks.loads(config_path.read_bytes(), option=yamlrocks.OPT_ROUND_TRIP)
|
||||
requirements: dict[str, str] = {}
|
||||
changes: list[str] = []
|
||||
for target in SYNC_TARGETS:
|
||||
if target.requirements_file not in requirements:
|
||||
requirements[target.requirements_file] = (
|
||||
root / target.requirements_file
|
||||
).read_text()
|
||||
version = read_requirement_version(
|
||||
requirements[target.requirements_file], target.package
|
||||
)
|
||||
if version is None:
|
||||
raise SyncError(
|
||||
f"{target.requirements_file}: no '{target.package}==' pin found"
|
||||
)
|
||||
|
||||
entry = find_repo_entry(doc, target.repo)
|
||||
prefix, current = current_rev(entry, target.repo)
|
||||
if current == version:
|
||||
continue
|
||||
changes.append(f"{target.package}: {current} -> {version}")
|
||||
entry["rev"] = f"{prefix}{version}"
|
||||
|
||||
if changes and write:
|
||||
config_path.write_bytes(doc.to_yaml())
|
||||
return changes
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="report drift without modifying any file; exit 1 if out of sync",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
default=REPO_ROOT,
|
||||
help="repository checkout to operate on (default: this checkout)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
changes = sync(args.root, write=not args.check)
|
||||
except SyncError as err:
|
||||
print(f"error: {err}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
for change in changes:
|
||||
print(change)
|
||||
if args.check and changes:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Unit tests for script/sync_dependency_versions.py."""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import yamlrocks
|
||||
|
||||
sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve()))
|
||||
|
||||
import sync_dependency_versions as sync_mod # noqa: E402
|
||||
|
||||
PRECOMMIT = """\
|
||||
# See https://pre-commit.com for more information
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.0.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v13.0.1
|
||||
hooks:
|
||||
- id: clang-format
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
"""
|
||||
|
||||
REQ_TEST = """\
|
||||
pylint==4.0.8
|
||||
flake8==7.1.0
|
||||
ruff==0.2.0 # comment
|
||||
pyupgrade==3.0.0
|
||||
"""
|
||||
|
||||
REQ_DEV = """\
|
||||
clang-format==13.0.1
|
||||
yamllint==1.0.0
|
||||
"""
|
||||
|
||||
RUFF_REPO = "https://github.com/astral-sh/ruff-pre-commit"
|
||||
DUPLICATE_RUFF_BLOCK = f" - repo: {RUFF_REPO}\n rev: v0.3.0\n hooks: []\n"
|
||||
|
||||
EXPECTED_DRIFT = ["ruff: 0.1.0 -> 0.2.0", "flake8: 7.0.0 -> 7.1.0"]
|
||||
EXPECTED_PRECOMMIT = PRECOMMIT.replace("rev: v0.1.0", "rev: v0.2.0").replace(
|
||||
"rev: 7.0.0", "rev: 7.1.0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path) -> Path:
|
||||
"""A fake checkout where ruff (v-prefixed) and flake8 (bare) have drifted."""
|
||||
(tmp_path / ".pre-commit-config.yaml").write_text(PRECOMMIT)
|
||||
(tmp_path / "requirements_test.txt").write_text(REQ_TEST)
|
||||
(tmp_path / "requirements_dev.txt").write_text(REQ_DEV)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _load(text: str) -> object:
|
||||
return yamlrocks.loads(text.encode(), option=yamlrocks.OPT_ROUND_TRIP)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("requirements", "expected"),
|
||||
[
|
||||
("prek==0.5.1 # comment\n", "0.5.1"),
|
||||
("Prek==0.5.1\n", "0.5.1"),
|
||||
("other==1.0\nprek==0.5.1\n", "0.5.1"),
|
||||
("prek>=0.5.1\n", None),
|
||||
("prek-extra==0.5.1\n", None),
|
||||
("", None),
|
||||
],
|
||||
)
|
||||
def test_read_requirement_version(requirements: str, expected: str | None) -> None:
|
||||
assert sync_mod.read_requirement_version(requirements, "prek") == expected
|
||||
|
||||
|
||||
def test_find_repo_entry() -> None:
|
||||
entry = sync_mod.find_repo_entry(_load(PRECOMMIT), RUFF_REPO)
|
||||
assert entry["rev"] == "v0.1.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "message"),
|
||||
[
|
||||
("hooks: []\n", "missing key 'repos'"),
|
||||
("repos:\n - rev: 1.0.0\n", "missing key 'repo'"),
|
||||
(PRECOMMIT + DUPLICATE_RUFF_BLOCK, "found 2"),
|
||||
("repos:\n - repo: other\n rev: 1.0.0\n", "found 0"),
|
||||
],
|
||||
)
|
||||
def test_find_repo_entry_errors(text: str, message: str) -> None:
|
||||
with pytest.raises(sync_mod.SyncError, match=message):
|
||||
sync_mod.find_repo_entry(_load(text), RUFF_REPO)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rev", "expected"),
|
||||
[("v0.1.0", ("v", "0.1.0")), ("7.0.0", ("", "7.0.0")), ("'1.0'", ("", "1.0"))],
|
||||
)
|
||||
def test_current_rev(rev: str, expected: tuple[str, str]) -> None:
|
||||
doc = _load(f"repos:\n - repo: {RUFF_REPO}\n rev: {rev}\n")
|
||||
assert sync_mod.current_rev(doc["repos"][0], RUFF_REPO) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("block", "message"),
|
||||
[(" hooks: []\n", "has no rev"), (" rev: 1.0\n", "not a string: 1.0")],
|
||||
)
|
||||
def test_current_rev_errors(block: str, message: str) -> None:
|
||||
doc = _load(f"repos:\n - repo: {RUFF_REPO}\n{block}")
|
||||
with pytest.raises(sync_mod.SyncError, match=message):
|
||||
sync_mod.current_rev(doc["repos"][0], RUFF_REPO)
|
||||
|
||||
|
||||
def test_sync_reports_without_writing(root: Path) -> None:
|
||||
assert sync_mod.sync(root, write=False) == EXPECTED_DRIFT
|
||||
assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT
|
||||
|
||||
|
||||
def test_sync_writes_keeps_layout_and_is_idempotent(root: Path) -> None:
|
||||
assert sync_mod.sync(root, write=True) == EXPECTED_DRIFT
|
||||
assert (root / ".pre-commit-config.yaml").read_text() == EXPECTED_PRECOMMIT
|
||||
assert sync_mod.sync(root, write=True) == []
|
||||
|
||||
|
||||
def test_sync_does_not_touch_a_config_that_matches(root: Path) -> None:
|
||||
(root / ".pre-commit-config.yaml").write_text(EXPECTED_PRECOMMIT)
|
||||
before = (root / ".pre-commit-config.yaml").stat().st_mtime_ns
|
||||
assert sync_mod.sync(root, write=True) == []
|
||||
assert (root / ".pre-commit-config.yaml").stat().st_mtime_ns == before
|
||||
|
||||
|
||||
def test_sync_missing_requirement_pin(root: Path) -> None:
|
||||
(root / "requirements_dev.txt").write_text("")
|
||||
with pytest.raises(sync_mod.SyncError, match="no 'clang-format==' pin"):
|
||||
sync_mod.sync(root, write=True)
|
||||
|
||||
|
||||
def test_sync_propagates_config_errors(root: Path) -> None:
|
||||
(root / ".pre-commit-config.yaml").write_text(PRECOMMIT + DUPLICATE_RUFF_BLOCK)
|
||||
with pytest.raises(sync_mod.SyncError, match="found 2"):
|
||||
sync_mod.sync(root, write=True)
|
||||
|
||||
|
||||
def test_main_check_reports_drift(
|
||||
root: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert sync_mod.main(["--check", "--root", str(root)]) == 1
|
||||
assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT
|
||||
assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT
|
||||
|
||||
|
||||
def test_main_writes_then_check_is_clean(
|
||||
root: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
assert sync_mod.main(["--root", str(root)]) == 0
|
||||
assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT
|
||||
assert sync_mod.main(["--check", "--root", str(root)]) == 0
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_main_reports_sync_error(
|
||||
root: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
(root / "requirements_dev.txt").write_text("")
|
||||
assert sync_mod.main(["--root", str(root)]) == 1
|
||||
assert (
|
||||
"error: requirements_dev.txt: no 'clang-format==' pin"
|
||||
in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
def test_main_defaults_to_repo_root(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
seen: dict[str, object] = {}
|
||||
|
||||
def fake_sync(root: Path, *, write: bool) -> list[str]:
|
||||
seen["root"] = root
|
||||
seen["write"] = write
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(sync_mod, "sync", fake_sync)
|
||||
assert sync_mod.main([]) == 0
|
||||
assert seen == {"root": sync_mod.REPO_ROOT, "write": True}
|
||||
|
||||
|
||||
def test_repository_is_in_sync() -> None:
|
||||
"""The real checkout must match; a failure here means a rev has drifted.
|
||||
|
||||
Also proves every SYNC_TARGETS entry still resolves in the real files.
|
||||
"""
|
||||
assert sync_mod.sync(sync_mod.REPO_ROOT, write=False) == []
|
||||
|
||||
|
||||
def test_cli_entry_point(root: Path) -> None:
|
||||
"""Run the script the way the workflow does, as a subprocess."""
|
||||
script = Path(sync_mod.__file__)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(script), "--check", "--root", str(root)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 1
|
||||
assert result.stdout.splitlines() == EXPECTED_DRIFT
|
||||
Reference in New Issue
Block a user