Skip to content

tests: Cover --hook-config=--tool-version resolution with pytest - #1004

Draft
MaxymVlasov wants to merge 12 commits into
downloadable_and_version_controlled_hooksfrom
downloadable_and_version_controlled_hooksatmos_append_2-tests
Draft

tests: Cover --hook-config=--tool-version resolution with pytest#1004
MaxymVlasov wants to merge 12 commits into
downloadable_and_version_controlled_hooksfrom
downloadable_and_version_controlled_hooksatmos_append_2-tests

Conversation

@MaxymVlasov

@MaxymVlasov MaxymVlasov commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Put an x into the box if that apply:

  • This PR introduces breaking change.
  • This PR fixes a bug.
  • This PR adds new functionality.
  • This PR enhances existing functionality.

Description of your changes

Why

Important

Somehow related/conflicts with #990. So far - I can't say what will be chosen, waiting for python-guru's review

Main idea - make some black-box testing for bash hooks on pytest, which then can be applied on hooks reimplementation on Python, when/if it time comes

Or if that's a shity approach - then just make some black-box testing, not in pytest

What

This section was generated by AI.

  • Add tests/pytest/tool_version_test.py: black-box pytest suite for the --hook-config=--tool-version=/--tool-version-mode=/--tf-path selector resolution added on downloadable_and_version_controlled_hooks. Tests invoke real hooks/*.sh scripts as subprocesses and assert on exit code / cache filesystem state / stdout+stderr only - never on bash-internal function names - so they need minimal changes if a hook is ever rewritten in another language.
  • Covers: cache-hit reuse (no network), --tool-version-mode=strict vs prefer-local, --tf-path=terraform|opentofu|tofu selector plus an invalid-value error case, the documented checkov no-op, the actionable "tool not found" error, and one real end-to-end download on a cache miss.
  • Add a .flake8 per-file-ignores entry for the new file (WPS202, WPS226), following the existing precedent already set for tests/pytest/_cli_test.py.
  • Full proposal/design/spec/tasks tracked in openspec/changes/test-tool-version-resolution/.

How can we test changes

This section was generated by AI.

  • pytest tests/pytest/tool_version_test.py -v locally (10/10 pass) and pytest tests/pytest/ -v for the full suite (21/21 pass, no regressions in the two pre-existing test modules).
  • This PR is opened against downloadable_and_version_controlled_hooks (not master) specifically so this repo's GH Actions matrix runs the suite for real - including the one network-dependent download test - rather than only locally.
  • All pre-commit hooks pass locally: ruff check, ruff format, wemake-python-styleguide, mypy (py3.10/3.12/3.14).

Assisted-by

Specific models used per commit are specified in the commit messages.

  • Assisted-by: Sisyphus:claude-sonnet-5 claude (a819bbf)

Adds tests/pytest/tool_version_test.py: black-box subprocess tests
against hooks/*.sh asserting on exit code, cache filesystem state,
and stdout/stderr - not on bash internals - so they need minimal
changes if hooks are ever rewritten in another language.

Covers: cache-hit reuse, --tool-version-mode=strict/prefer-local,
--tf-path=terraform/opentofu/tofu selector (+ invalid value), the
checkov no-op, the actionable not-found error, and one real
cache-miss download.

Opened against downloadable_and_version_controlled_hooks (not
master) specifically to prove these tests actually run green in
this repo's GH Actions matrix, not just locally.

Adds a .flake8 per-file-ignores entry for the new test file
(WPS202/WPS226), following the same precedent already set for
tests/pytest/_cli_test.py.

See openspec/changes/test-tool-version-resolution/ for the full
proposal/design/spec/tasks.

Assisted-by: Sisyphus:claude-sonnet-5 claude
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 98ef5ef8-7730-48a1-9d49-577d62f0e0a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch downloadable_and_version_controlled_hooksatmos_append_2-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The 'tests' CI job runs pytest against a pre-built sdist tarball
(via re-actors/checkout-python-sdist), not a git checkout. hatch.toml
whitelisted only src/, tests/, and a few config files for the sdist,
so hooks/*.sh - which the new tool_version_test.py invokes as
subprocesses - never existed in that job's working directory.

Add hooks/ to [build.targets.sdist].include.

Also harden tool_version_test.py against this exact failure mode
recurring silently:
- _run_hook() now fails fast with a clear message if the resolved
  hook script doesn't exist, instead of letting a generic bash
  'No such file or directory' surface only as an unrelated,
  confusing assertion mismatch several lines down.
- Strengthened the two tests whose original assertions only checked
  for absence of specific substrings (never for a successful exit),
  so they could vacuously pass even when the hook script failed to
  execute at all. This is exactly how the CI failure above stayed
  undetected in 8 of the other tests failed loudly while these 2
  passed for the wrong reason.

Restructure the test file into two pytest classes
(TestCacheAndModeResolution, TestTfPathSelectorAndErrorHandling)
instead of adding a WPS202 per-file-ignore to .flake8: grouping
methods this way keeps module-level members at 7 (the wemake-
python-styleguide default limit) without suppressing the check or
reducing test granularity. Only WPS226 (string-literal overuse, same
precedent as tests/pytest/_cli_test.py) remains ignored for this
file.

Assisted-by: Sisyphus:claude-sonnet-5 claude
hooks/_common.sh also sources '$script_dir/../lib_getopt' and, for
--tool-version resolution specifically, reads installer scripts from
'$script_dir/../tools/install/${tool_name}.sh' - both live at the repo
root, sibling to hooks/, so adding hooks/ alone (previous commit)
wasn't enough; CI still failed with 'lib_getopt: No such file or
directory' from inside terraform_tflint.sh's sourced _common.sh.

Verified this time by actually building the sdist locally
(python -m build --sdist), extracting it to a clean directory, and
running the full pytest suite against only that extracted tree
(nothing from the live repo) - the exact scenario CI runs. All 21
tests pass against the real sdist contents, not just against my
working tree.

Assisted-by: Sisyphus:claude-sonnet-5 claude
hooks/_common.sh and tools/install/_common.sh both used
"${var^^}" for uppercasing (e.g. tflint -> TFLINT_VERSION), which
is bash 4+ only. macOS ships bash 3.2.57 by default (Apple's last
GPLv2 release) and this repo explicitly documents 'BASH 3.2.57 or
newer' as a supported baseline (README.md) - CI's macOS runner hit
exactly this with 'bad substitution' once the sdist-packaging fix
let it reach this code for the first time.

Replaced both with a portable 'tr' pipeline. Also fix
tools/install/opentofu.sh's GH_RELEASE_REGEX_SPECIFIC_VERSION: it
required a literal 'v' immediately before the version in the asset
*filename* (only the release *tag* has that 'v', e.g.
tofu_1.12.5_linux_amd64.tar.gz has no 'v'), so --tool-version pinning
for opentofu specifically always fell through to a blank curl URL.
(Same fix as already proposed standalone in PR #1001 against master;
duplicated here since this branch predates that PR and my test
suite's real-download coverage was tflint-only, so it never
exercised this path.)

Verified all three fixes against a real bash 3.2.57 (docker run
bash:3.2, matching macOS's exact version) end-to-end: real tflint
and real opentofu downloads both succeed and produce a working,
correctly-versioned cached binary.

Assisted-by: Sisyphus:claude-sonnet-5 claude
test_real_download_on_cache_miss makes a real, unauthenticated
api.github.com call to resolve tflint's latest-releases listing.
GITHUB_TOKEN was never actually forwarded into the pytest job's
environment - tox.ini's 'pass_env = GITHUB_*' can only pick up a
variable that's already there, and reusable-tox.yml never set it.

With a 5-python x 4-OS matrix (20 jobs) all potentially sharing
GitHub-hosted runners' pooled egress IPs, unauthenticated requests
(60/hr per IP, shared with unrelated concurrent traffic) are a
well-known source of exactly this kind of intermittent, job-specific
failure - matches what was observed: a fast (~0.3s) failure on one
matrix cell (pytest-3.12) while the rest of the matrix passed.

This wires in the same GITHUB_TOKEN auto-authentication the project
already documents as supported for version resolution (README.md,
'Most hooks: Pin a specific tool version') and already uses for
Docker image builds - it was simply never connected for this job.

Assisted-by: Sisyphus:claude-sonnet-5 claude
covdefaults enforces fail_under=100; my new test file was at 98.32%
on two genuinely different gaps:

- _fake_path_dir's 'hide' parameter and its exclusion check were
  dead code by construction: the only caller passes hide='tflint',
  which was never a member of the coreutils tuple to begin with, so
  the exclusion branch could never fire. Removed the parameter
  entirely rather than paper over it - none of these coreutil names
  will ever collide with a wrapped CLI tool name.
- The 'found is not None' guard around each coreutil's shutil.which()
  result was also dead in every realistic CI target (ubuntu-24.04,
  macos-15/-intel all ship every one of these 9 tools; Windows is
  already skipped module-wide). Replaced the silent skip with an
  explicit assert, which also satisfies mypy's arg-type check on
  Path.symlink_to (str | None isn't assignable to str) and fails
  loudly instead of quietly building a broken PATH if a future CI
  image is ever missing one of them.

_run_hook's own 'hook script not found' fail-fast guard is different:
it's a genuine, load-bearing diagnostic for a failure mode that has
already recurred multiple times in this exact CI (sdist packaging).
By definition it can never fire in a passing run, and simulating a
broken environment just to exercise it would test the test harness
itself rather than the tool-version feature. Marked
'# pragma: no cover' with an explanation, rather than writing a test
whose only purpose is satisfying the coverage tool.

Verified 100% via a bypassed-config coverage run locally (this
sandbox's coverage/covdefaults versions can't parse .coveragerc's
newer exclude_also option, so ran with --rcfile=/dev/null against
just tests/pytest/ instead).

Assisted-by: Sisyphus:claude-sonnet-5 claude
On win32, the whole module's tests are skipped (pytestmark), so none
of the file's own helpers/fixtures/test bodies ever execute - that's
by design (this repo doesn't guarantee Windows hook execution), but
it meant total coverage on the windows-2025 CI job cratered to
69.79%, well under covdefaults' fail_under=100.

covdefaults already ships exactly the mechanism for this: platform
pragmas built from {os.name, sys.platform, sys.implementation.name}.
'# pragma: win32 no cover' excludes a def/class (and everything
nested in it) from the coverage requirement only when actually
running on win32 - on every other platform it's inert and the code
is still required to be covered normally.

Applied it to every def/class in this file that's unreachable on
Windows: the two helpers, both fixtures, _run_hook, and both test
classes (methods don't need their own pragma - excluding a class
line cascades to its methods, verified empirically below).

Verified properly this time by actually installing a modern coverage
(>=7.2.2, understands .coveragerc's exclude_also) and covdefaults
locally, rather than reasoning from stale/bypassed local tooling:
100% coverage, 0 missed statements/branches, using the project's
real .coveragerc as-is. Also read covdefaults' own source
(_plat_impl_pragmas) to confirm the activation logic rather than
trusting docs alone: on Linux, 'win32' isn't in the running
platform's tag set, so 'pragma: win32 no cover' lines stay normal,
required, covered code here - matching what was observed (still
100% with zero regressions).

Assisted-by: Sisyphus:claude-sonnet-5 claude
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.02%. Comparing base (2ed31af) to head (53cb921).

Additional details and impacted files
@@                              Coverage Diff                              @@
##           downloadable_and_version_controlled_hooks    #1004      +/-   ##
=============================================================================
+ Coverage                                      96.53%   98.02%   +1.48%     
=============================================================================
  Files                                             10       11       +1     
  Lines                                            260      455     +195     
  Branches                                           7       10       +3     
=============================================================================
+ Hits                                             251      446     +195     
  Misses                                             9        9              
Flag Coverage Δ
CI-GHA 98.02% <100.00%> (+1.48%) ⬆️
MyPy 95.13% <100.00%> (+3.69%) ⬆️
OS-Linux 98.02% <100.00%> (+1.48%) ⬆️
OS-Windows 100.00% <100.00%> (ø)
OS-macOS 100.00% <100.00%> (ø)
Py-3.10.11 100.00% <100.00%> (ø)
Py-3.10.20 100.00% <100.00%> (ø)
Py-3.11.15 100.00% <100.00%> (ø)
Py-3.11.9 100.00% <100.00%> (ø)
Py-3.12.10 100.00% <100.00%> (ø)
Py-3.12.13 100.00% <100.00%> (ø)
Py-3.13.14 98.02% <100.00%> (+1.48%) ⬆️
Py-3.14.6 100.00% <100.00%> (ø)
VM-macos-15 100.00% <100.00%> (ø)
VM-macos-15-intel 100.00% <100.00%> (ø)
VM-ubuntu-24.04 100.00% <100.00%> (ø)
VM-ubuntu-latest 95.13% <100.00%> (+3.69%) ⬆️
VM-windows-2025 100.00% <100.00%> (ø)
pytest 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@MaxymVlasov MaxymVlasov changed the title test: cover --tool-version resolution with pytest tests: Cover --hook-config=--tool-version resolution with pytest Jul 24, 2026
@MaxymVlasov
MaxymVlasov force-pushed the downloadable_and_version_controlled_hooks branch from b55dcfc to e24e21b Compare July 24, 2026 15:24
…dable_and_version_controlled_hooksatmos_append_2-tests
…dable_and_version_controlled_hooksatmos_append_2-tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant