tests.test_tool_runner module

Tests for the unified tool runner.

tests.test_tool_runner.test_tool_spec_integrity(name, spec)[source]

Each registry entry passes structural integrity checks.

tests.test_tool_runner.test_bundled_actionlint_labels_name_runners_the_matrix_emits()[source]

Every declared label answers a runner the generated workflows can name.

actionlint validates runs-on: against a list baked into its binary, and rejects a GitHub-hosted label its release predates. The bundled config declares those through self-hosted-runner, actionlint’s escape hatch for an unrecognized label, which is why the entries carry no implication of being self-hosted.

Pinning them to the matrix constants is what keeps the file from rotting: an entry surviving a move to a newer image would go on whitelisting a runner nothing uses, and read as still load-bearing.

tests.test_tool_runner.test_tool_backend_labels_unique()[source]

Every backend resolves distinct display labels for the docs tables.

tests.test_tool_runner.test_tool_backend_matches_payload_fields(name, spec)[source]

Each registry entry maps onto exactly one delivery backend.

tests.test_tool_runner.test_build_install_args_cooldown(name, spec, tmp_path, monkeypatch)[source]

uvx tools carry --exclude-newer; needs_venv tools only when unlocked.

tests.test_tool_runner.test_uvx_tool_pins_registry_version(name, spec, tmp_path, monkeypatch)[source]

Every uvx-installed tool pins its TOOL_REGISTRY version in the command.

Locks the invariant that keeps the per-tool command tests drift-proof: a version bump in TOOL_REGISTRY must flow into the name==version pin with no hand-edited literal. A hard-coded name==X.Y.Z in an individual test silently goes stale on the next bump (a missed pyproject-fmt literal once reddened every Tests cell), so version-pinning is asserted here once, generically, against the shared builder rather than restated per tool.

The pin sits in the command that installs the tool. With a uv.lock, a tool declaring install_into_venv is installed by its own command.

tests.test_tool_runner.test_build_install_args_install_into_venv(name, spec, tmp_path, monkeypatch)[source]

A tool installed into the project venv runs from it, with no layer.

tests.test_tool_runner.test_install_into_venv_installs_the_pin(mock_run, name, spec)[source]

The pinned tool goes into the venv that uv run --frozen reports.

tests.test_tool_runner.test_install_into_venv_reports_a_uv_failure(mock_run, results, message)[source]

A uv failure stops the run before the tool starts.

tests.test_tool_runner.test_datasource_url_by_backend()[source]

datasource_url points at npmjs for npm tools, GitHub/PyPI otherwise.

tests.test_tool_runner.test_install_npm_command_shape(mock_which, mock_run, tmp_path)[source]

The npm backend installs pkg@version into a prefix with the cooldown flag.

tests.test_tool_runner.test_install_npm_zero_cooldown_omits_flag(mock_which, mock_run, tmp_path)[source]

A 0-day cooldown drops the min-release-age flag entirely.

tests.test_tool_runner.test_install_npm_without_npm_raises(mock_which, tmp_path)[source]

A clear error surfaces when Node.js and npm are not on PATH.

tests.test_tool_runner.test_npm_supports_cooldown(mock_run, version_output, supported)[source]

min-release-age support is gated on npm >= 11.10.0.

tests.test_tool_runner.test_install_npm_warns_when_npm_too_old(mock_which, mock_run, tmp_path, caplog)[source]

Old npm cannot enforce the cooldown, so the runner warns but still installs.

tests.test_tool_runner.test_tool_registry_sorted_alphabetically()[source]

Registry keys are sorted alphabetically.

tests.test_tool_runner.test_pypi_name_strips_install_extra()[source]

pypi_name yields the bare PyPI project name, dropping any install extra.

Nuitka pins package="nuitka[onefile]" for installation, but the PyPI JSON API 404s on the bracketed name, so pypi_name must return nuitka for sync-tool-versions to find the real project.

tests.test_tool_runner.test_pypi_pin_names_the_package_and_its_version(package, pin)[source]

pypi_pin is the exact requirement an installer takes, extra included.

The command tests above read the pin from this property, so its shape is stated here once.

tests.test_tool_runner.test_checksum_sidecar_covers_exactly_binary_tools()[source]

CHECKSUMS and VERSIONS are keyed by exactly the binary tool names.

tests.test_tool_runner.test_checksum_version_matches_registry(name)[source]

Each tool’s sidecar version stamp matches its ToolSpec.version.

The offline tripwire: a version bump that did not refresh the checksums leaves VERSIONS stale, failing here before the release is cut. Fix by running repomatic update-checksums.

tests.test_tool_runner.test_resolve_platform_exact_match()[source]

Exact Platform match takes priority over Group membership.

tests.test_tool_runner.test_resolve_platform_group_match()[source]

Group membership matches when no exact Platform key exists.

tests.test_tool_runner.test_resolve_platform_no_match()[source]

No matching key raises RuntimeError.

tests.test_tool_runner.test_resolve_platform_unknown_distro_falls_back_to_linux()[source]

An unidentified distribution still reaches the family-wide LINUX key.

extra-platforms returns UNKNOWN_PLATFORM for any distribution it has yet to learn, and that sentinel belongs to no group, so the group pass misses. The manylinux_2_28 build container (AlmaLinux) is the motivating case: it broke every repomatic run binary on a distro identity that never entered the choice of binary.

tests.test_tool_runner.test_resolve_platform_unknown_distro_refuses_beyond_linux(urls, sys_platform)[source]

The fallback widens to the Linux family and no further.

tests.test_tool_runner.test_linux_binaries_are_keyed_on_the_family_group()[source]

Every tool shipping a Linux binary keys it on the LINUX group.

The pass-3 fallback only rescues a family-wide key, so a tool keyed on a specific distribution instead would still fail inside the manylinux build container.

tests.test_tool_runner.test_platform_cache_key()[source]

Cache key is a filesystem-safe string derived from the PlatformKey.

tests.test_tool_runner.test_download_and_verify_success(tmp_path)[source]

Successful download with matching checksum writes the file.

tests.test_tool_runner.test_download_and_verify_no_content_length(tmp_path)[source]

A response without Content-Length downloads via the indeterminate Spinner.

tests.test_tool_runner.test_download_and_verify_mismatch(tmp_path)[source]

Checksum mismatch raises ValueError and cleans up.

tests.test_tool_runner.test_download_and_verify_retries_transient_failure(tmp_path)[source]

A transient network failure is retried and the next attempt succeeds.

tests.test_tool_runner.test_download_and_verify_gives_up_after_attempts(tmp_path)[source]

A persistent network failure surfaces after the last attempt.

tests.test_tool_runner.test_download_and_verify_truncated(tmp_path)[source]

A body shorter than Content-Length raises OSError, not a mismatch.

A truncated transfer (proxy hiccup, dropped connection) hashes to a wrong digest, so it used to be reported as a SHA-256 mismatch: that reads as a stale pin or a tampered artifact when the registry checksum is correct. Truncation is retried like any network failure, so the error only surfaces once every attempt came up short.

tests.test_tool_runner.test_extract_binary_raw(tmp_path)[source]

RAW format renames the archive to the executable name.

tests.test_tool_runner.test_extract_binary_tar_gz(tmp_path)[source]

TAR_GZ extracts the named executable and makes it executable.

tests.test_tool_runner.test_extract_binary_tar_gz_with_strip_components(tmp_path)[source]

TAR_GZ with strip_components strips leading path components.

tests.test_tool_runner.test_extract_binary_tar_xz(tmp_path)[source]

TAR_XZ extracts the named executable.

tests.test_tool_runner.test_extract_binary_tar_missing_executable(tmp_path)[source]

Missing executable in tar archive raises FileNotFoundError.

tests.test_tool_runner.test_extract_binary_tar_unsafe_path(tmp_path)[source]

Archive member with path traversal raises ValueError.

tests.test_tool_runner.test_extract_binary_zip(tmp_path)[source]

ZIP format extracts the named executable and makes it executable.

tests.test_tool_runner.test_extract_binary_zip_with_strip_components(tmp_path)[source]

ZIP with strip_components strips leading path components.

tests.test_tool_runner.test_extract_binary_zip_missing_executable(tmp_path)[source]

Missing executable in ZIP archive raises FileNotFoundError.

tests.test_tool_runner.test_extract_binary_zip_unsafe_path(tmp_path)[source]

ZIP member with path traversal raises ValueError.

tests.test_tool_runner.test_extract_binary_format_override(tmp_path)[source]

Per-platform archive format from dict is used when passed.

tests.test_tool_runner.test_get_archive_format_dict_resolution()[source]

Dict archive_format resolves Platform > Group membership.

tests.test_tool_runner.test_install_binary_missing_platform()[source]

Missing platform key in binary spec raises RuntimeError.

tests.test_tool_runner.test_path_tools_env_skips_platform_without_binary(caplog)[source]

A companion publishing no binary here is skipped, not fatal.

shfmt ships nothing for Windows ARM64, so provisioning it for mdformat can never succeed there. Aborting would make repomatic run mdformat unusable on the platform rather than merely leaving its shell blocks unformatted, so the run carries on with PATH untouched.

tests.test_tool_runner.TARBALL_URL = 'https://example.com/{version}/tool.tar.gz'

Download URL for the tests that go through the extract step.

tests.test_tool_runner.test_install_binary_cache_hit(tmp_path, monkeypatch, cache_env)[source]

_install_binary returns cached path when cache hit and sidecar matches.

tests.test_tool_runner.test_install_binary_cache_hit_on_a_nested_archive_executable(tmp_path, monkeypatch, cache_env, cached_name)[source]

A cached entry is found whatever path the executable held in the archive.

archive_executable names a location inside the archive, while the cache keys an entry on the extracted file’s own name. Probing with the nested path matched nothing, so a tool declaring one re-downloaded on every call despite a complete, sidecar-verified copy sitting in the cache. gh (bin/gh) was the only registry tool to declare one, and since every gh invocation routes through the pinned binary, it re-fetched 13 MB per run.

tests.test_tool_runner.test_install_binary_cache_miss_stores(tmp_path, monkeypatch, cache_env)[source]

_install_binary stores the binary in cache after download on miss.

tests.test_tool_runner.test_install_binary_no_cache_flag(tmp_path, monkeypatch, cache_env)[source]

_install_binary with no_cache=True bypasses cache entirely.

tests.test_tool_runner.test_install_binary_cache_integrity_failure(tmp_path, monkeypatch, cache_env)[source]

_install_binary re-downloads when cached binary fails sidecar check.

tests.test_tool_runner.test_install_binary_cache_store_fallback(tmp_path, monkeypatch, cache_env)[source]

_install_binary falls back to temp path when cached file is missing.

tests.test_tool_runner.test_run_tool_binary_uses_direct_path(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

Binary tools use the downloaded binary path, not uvx.

tests.test_tool_runner.test_run_tool_holds_excludes_for_explicit_paths(mock_ci, mock_install, mock_run, tmp_path, monkeypatch, name, flag, extra_args)[source]

A path named on the command line stays subject to the tool’s exclude list.

ruff and typos apply their exclude list only while they walk a directory, so a file passed by name is processed whatever the list says, unless the flag that holds the list comes with it.

tests.test_tool_runner.test_run_tool_binary_forwards_extra_args(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

Extra args are appended after default flags for binary tools.

tests.test_tool_runner.test_run_tool_binary_default_flags(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

Binary tools include default_flags in the command.

tests.test_tool_runner.test_run_tool_ruff_bundled_default(mock_ci, mock_run, tmp_path, monkeypatch)[source]

ruff uses bundled default config when no config exists.

tests.test_tool_runner.test_run_tool_ruff_reads_pyproject_natively(mock_ci, mock_run, tmp_path, monkeypatch)[source]

ruff gets no config file when [tool.ruff] exists in pyproject.toml.

tests.test_tool_runner.test_run_tool_bump_my_version_via_uvx(mock_ci, mock_run, tmp_path, monkeypatch)[source]

bump-my-version runs via uvx with subcommand extra_args.

tests.test_tool_runner.test_ensure_binary_memoizes_per_tool(mock_install, tmp_path)[source]

Repeated calls install once and return the same path.

Callers in a loop (format-images optimizing one PNG per call) must not pay the install-and-verify path once per file.

tests.test_tool_runner.test_ensure_binary_cleans_staging_on_cache_hit(mock_install, tmp_path)[source]

When the install lands in the cache, the staging directory is removed.

tests.test_tool_runner.test_ensure_binary_keeps_staging_fallback_alive(mock_install)[source]

A staging-path fallback survives the call instead of dangling.

When the cache store fails (unwritable root, Docker overlay losing the write), _install_binary returns the staging copy: the staging directory must then outlive the call, or the caller would exec a just-deleted file.

tests.test_tool_runner.test_resolve_config_reads_pyproject_with_section()[source]

Tools with reads_pyproject=True skip translation when [tool.X] exists.

tests.test_tool_runner.test_resolve_config_reads_pyproject_falls_through_to_bundled(tmp_path, monkeypatch, cache_env)[source]

Tools with reads_pyproject=True use bundled default when no config exists.

tests.test_tool_runner.test_resolve_config_native_file_wins(tmp_path, monkeypatch)[source]

Native config file takes precedence over everything else.

tests.test_tool_runner.test_resolve_config_pyproject_section(tmp_path, monkeypatch, cache_env)[source]

[tool.X] in pyproject.toml produces a cached config file.

tests.test_tool_runner.test_resolve_config_toml_translation(tmp_path, monkeypatch, cache_env)[source]

[tool.X] with native_format=’toml’ produces a valid TOML cached file.

tests.test_tool_runner.test_resolve_config_toml_nested_tables(tmp_path, monkeypatch, cache_env)[source]

Nested dicts in [tool.X] produce TOML table sections.

tests.test_tool_runner.test_resolve_config_toml_preserves_pyproject_comments(tmp_path, monkeypatch)[source]

A [tool.X] TOML section materialised to a native file keeps its comments.

Reading the section live (not via a plain-dict copy) lets the generated standalone file carry the user’s comments while dropping the [tool.X] prefix.

tests.test_tool_runner.test_native_format_toml_serialize_reroots_section_and_keeps_comments()[source]

A live [tool.X] section loses its prefix and keeps every comment.

tests.test_tool_runner.test_native_format_toml_serialize_inline_section()[source]

An inline tool.x = {...} table serializes to the same data.

tests.test_tool_runner.test_resolve_config_json_translation(tmp_path, monkeypatch, cache_env)[source]

[tool.X] with native_format=’json’ produces a valid JSON cached file.

tests.test_tool_runner.test_resolve_config_cwd_write_no_config_flag(tmp_path, monkeypatch)[source]

CWD-discovery tools write translated [tool.X] to the native config path.

tests.test_tool_runner.test_resolve_config_no_config_flag_no_native_files_raises(tmp_path, monkeypatch)[source]

Tools with no config_flag and no native_config_files raise NotImplementedError.

tests.test_tool_runner.test_resolve_config_bundled_default(tmp_path, monkeypatch, cache_env)[source]

Bundled default is cached and passed via –config flag.

tests.test_tool_runner.test_resolve_config_bare_invocation(tmp_path, monkeypatch)[source]

Tool with no config at all gets bare invocation.

tests.test_tool_runner.test_resolve_config_empty_tool_config_is_not_match(tmp_path, monkeypatch, cache_env)[source]

An empty [tool.X] dict does not count as a config match.

tests.test_tool_runner.BARE_TOOL = ToolSpec(name='sometool', display_name=None, version='1.0.0', package='sometool', executable=None, module=None, native_config_files=(), config_flag=None, native_format=<NativeFormat.YAML: 'yaml'>, default_config=None, reads_pyproject=False, default_flags=(), ci_flags=(), default_args=(), default_paths=None, per_file=False, with_packages=(), path_tools=(), needs_venv=False, install_into_venv=False, computed_params=None, config_after_subcommand=False, post_process=None, output_flag=None, check_flags=(), rewrite_exit_code=None, binary=None, npm=None, source_url=None, tag_pattern=None, config_docs_url=None, cli_docs_url=None, docs_notes='')

A tool with no bundled default, no native file and no pyproject section.

tests.test_tool_runner.test_resolve_config_source(tmp_path, monkeypatch, tool, files, expected)[source]

The reported config source names where the tool’s settings came from.

tests.test_tool_runner.test_run_tool_unknown_tool()[source]

Raise ValueError for unregistered tool names.

tests.test_tool_runner.test_run_tool_yamllint_bundled_default(mock_ci, mock_run, tmp_path, monkeypatch)[source]

yamllint with bundled default builds the correct command.

tests.test_tool_runner.test_run_tool_ci_flags(mock_ci, mock_run, tmp_path, monkeypatch)[source]

CI flags are appended when GITHUB_ACTIONS is set.

tests.test_tool_runner.test_run_tool_native_config_no_extra_flags(mock_ci, mock_run, tmp_path, monkeypatch)[source]

Tool with native config file gets no config flags from repomatic.

tests.test_tool_runner.test_run_tool_pyproject_section_cached_config(mock_ci, mock_run, tmp_path, monkeypatch, cache_env)[source]

[tool.X] translation writes config to cache and passes via –config.

tests.test_tool_runner.test_run_tool_forwards_exit_code(mock_ci, mock_run, tmp_path, monkeypatch)[source]

Tool’s exit code is forwarded unchanged.

tests.test_tool_runner.test_run_tool_autopep8_default_flags(mock_ci, mock_run, tmp_path, monkeypatch)[source]

autopep8 runs with all default flags via uvx.

tests.test_tool_runner.test_run_tool_pyproject_fmt(mock_ci, mock_run, tmp_path, monkeypatch)[source]

pyproject-fmt runs bare via uvx, with no forced –expand-tables flag.

tests.test_tool_runner.test_run_tool_mdformat_with_packages(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

mdformat runs via uvx with all plugin packages.

tests.test_tool_runner.test_run_tool_concurrent_runs_keep_their_cwd_config(mock_ci, mock_install, cache_env, tmp_path, monkeypatch)[source]

Two runs sharing a directory each keep a written config for their whole run.

Without the lock, the second run took the first run’s temporary .mdformat.toml for the repository’s own, then lost it halfway when the first run deleted it on the way out.

tests.test_tool_runner.test_run_tool_runs_unlocked_when_the_cache_is_unwritable(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

A lock file that cannot be created does not stop the run.

tests.test_tool_runner.test_run_tool_mypy_with_computed_params(mock_ci, mock_metadata_cls, mock_run, tmp_path, monkeypatch)[source]

mypy runs via uv run with computed –python-version param.

tests.test_tool_runner.test_run_tool_mypy_without_computed_params(mock_ci, mock_metadata_cls, mock_run, tmp_path, monkeypatch)[source]

mypy runs without computed params when Metadata returns None.

tests.test_tool_runner.test_run_tool_mypy_no_lockfile_falls_back_isolated(mock_ci, mock_metadata_cls, mock_run, tmp_path, monkeypatch)[source]

Without a uv.lock, mypy runs isolated instead of demanding --frozen.

A repository holding a pyproject.toml with only [tool.*] tables (a dotfiles repo linting standalone scripts) has no lockfile, so uv run --frozen aborts with “Unable to find lockfile at uv.lock” before mypy ever starts.

tests.test_tool_runner.test_run_tool_nuitka_uses_module_invocation(mock_ci, mock_run, tmp_path, monkeypatch)[source]

nuitka runs via python -m nuitka to avoid Windows script resolution issues.

tests.test_tool_runner.test_run_tool_nuitka_nofollow_imports_default(mock_ci, mock_run, tmp_path, monkeypatch)[source]

nuitka excludes tkinter by default via nuitka.nofollow-imports.

tests.test_tool_runner.test_run_tool_nuitka_nofollow_imports_override(mock_ci, mock_run, tmp_path, monkeypatch)[source]

An explicit nuitka.nofollow-imports replaces the tkinter default.

tests.test_tool_runner.test_run_tool_creates_output_parent_directory(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

run_tool creates parent directories for –output file paths.

tests.test_tool_runner.test_run_tool_creates_output_parent_equals_form(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

The --output=path spelling gets its parent directory created too.

tests.test_tool_runner.test_run_tool_output_existing_directory_is_noop(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

run_tool does not fail when –output parent directory already exists.

tests.test_tool_runner.test_run_tool_no_output_flag_skips_mkdir(mock_ci, mock_run, tmp_path, monkeypatch)[source]

run_tool without –output does not create any directories.

tests.test_tool_runner.test_get_data_file_path_existing()[source]

Bundled data files are accessible via get_data_file_path.

tests.test_tool_runner.test_get_data_file_path_missing()[source]

Missing data files raise FileNotFoundError.

tests.test_tool_runner.test_find_unmodified_configs_exact_match(tmp_path, monkeypatch)[source]

Native config file matching bundled default is flagged as unmodified.

tests.test_tool_runner.test_find_unmodified_configs_trailing_whitespace(tmp_path, monkeypatch)[source]

Trailing whitespace differences are normalized away.

tests.test_tool_runner.test_find_unmodified_configs_honors_root(tmp_path, monkeypatch)[source]

Detection reads under root, never the current directory.

run_init joins the returned paths against its output_dir, so a scan anchored elsewhere would report on one tree and let --delete-unmodified delete from another.

tests.test_tool_runner.test_find_unmodified_configs_modified_content(tmp_path, monkeypatch)[source]

Native config with different content is not flagged.

tests.test_tool_runner.test_find_unmodified_configs_no_file(tmp_path, monkeypatch)[source]

No native config on disk returns empty list.

tests.test_tool_runner.test_find_unmodified_configs_multiple_tools(tmp_path, monkeypatch)[source]

Redundant files for multiple tools are all detected.

tests.test_tool_runner.test_find_unmodified_configs_alternative_filename(tmp_path, monkeypatch)[source]

Alternative native config filename (.yamllint.yml) is also checked.

tests.test_tool_runner.test_directive_yaml_options_regex(before, after)[source]

YAML-block directive options are converted to field-list syntax.

tests.test_tool_runner.test_directive_yaml_options_regex_no_false_positives(content)[source]

Non-directive YAML blocks and horizontal rules are left untouched.

tests.test_tool_runner.test_fix_myst_directives_in_place(tmp_path)[source]

Post-processor rewrites files in-place and skips unchanged files.

tests.test_tool_runner.test_fix_myst_directives_multiple_directives(tmp_path)[source]

Multiple directive blocks in the same file are all fixed.

tests.test_tool_runner.test_escaped_colon_fence_regex(before, after)[source]

Escaped colon-fence directives are un-escaped, nesting included.

tests.test_tool_runner.test_escaped_colon_fence_regex_no_false_positives(content)[source]

Unescaped fences, deflist syntax and stray escapes are left untouched.

tests.test_tool_runner.test_fix_myst_directives_colon_fence_in_place(tmp_path)[source]

Both fixups run together: YAML options and escaped colon fences.

tests.test_tool_runner.test_fix_myst_directives_skips_a_file_that_is_not_text(tmp_path)[source]

A file that does not decode as UTF-8 is not one mdformat wrote.

run_tool hands over every file whose content changed below a directory argument, whoever wrote it: an editor that refreshes .git/index during the run is enough. The page listed after the photo proves the loop goes on.

tests.test_tool_runner.test_check_bypasses_post_process()[source]

check_bypasses_post_process is True only for a post_process tool in check mode.

tests.test_tool_runner.test_run_tool_warns_when_check_bypasses_post_process(mock_ci, mock_install, mock_run, tmp_path, monkeypatch, caplog)[source]

mdformat –check warns that the post-process fixup is skipped.

tests.test_tool_runner.test_run_tool_no_check_warning_in_write_mode(mock_ci, mock_install, mock_run, tmp_path, monkeypatch, caplog)[source]

mdformat in write mode (no check flag) emits no check-mode warning.

tests.test_tool_runner.test_run_tool_post_processes_files_below_a_directory(mock_ci, mock_install, mock_run, tmp_path, monkeypatch)[source]

mdformat pointed at a directory gets its fixup on each file it rewrote.

A file the run left alone keeps its content. It stands for a file that the exclude list of the tool skipped, which the fixup must not rewrite either.

tests.test_tool_runner.test_resolve_config_flags_format()[source]

A FLAGS-format spec turns [tool.X] into CLI args, not a file.

tests.test_tool_runner.test_resolve_config_flags_format_empty()[source]

No [tool.X] section yields no flags and no cleanup file.

tests.test_tool_runner.test_native_format_flags_serialize_raises()[source]

FLAGS is not a file format, so serialize() rejects it.

tests.test_tool_runner.test_strip_components_resolves_per_platform()[source]

A dict strip_components resolves like archive_format does.

gh is the reason both exist: its Linux and macOS archives nest under a versioned directory while the Windows zip does not, so one count cannot serve every platform.

tests.test_tool_runner.test_strip_components_plain_int_applies_everywhere()[source]

A bare int keeps applying to every platform, as before.

tests.test_tool_runner.test_gh_spec_matches_upstream_archive_layout()[source]

gh’s declared layout matches what cli/cli actually publishes.

Locks in the asymmetry that motivated per-platform strip_components: a future release that flattened the Linux tarball, or nested the Windows zip, would extract the wrong path and only fail during a release.

tests.test_tool_runner.test_ensure_binary_rejects_unknown_tool()[source]

An unregistered name fails loudly rather than falling back to $PATH.

tests.test_tool_runner.test_ensure_binary_rejects_non_binary_tool()[source]

A uvx- or npm-backed tool has no binary to place, so it is refused.

ruff installs through uvx; asking for its executable path would silently return nothing useful, so the seam rejects it instead.

tests.test_tool_runner.test_image_optimizers_resolve_through_registry_or_path()[source]

oxipng comes from the registry; jpegoptim still needs $PATH.

Guards the split documented in _check_tool: oxipng ships prebuilt binaries and is pinned and checksummed, while jpegoptim publishes source only and stays a distro package.

tests.test_tool_runner.test_resolve_default_args_without_defaults()[source]

A tool declaring none runs bare, exactly as before.

tests.test_tool_runner.test_resolve_default_args_flags_only()[source]

A tool whose CI shape is a literal argument needs no inventory.

tests.test_tool_runner.test_resolve_default_args_appends_the_inventory(tmp_path, monkeypatch)[source]

Declared paths resolve to the repository’s own matching files.

tests.test_tool_runner.test_resolve_default_args_keeps_default_args_ahead_of_paths(tmp_path, monkeypatch)[source]

A subcommand stays the first token, where the tool’s parser wants it.

tests.test_tool_runner.test_resolve_default_args_splits_per_file(tmp_path, monkeypatch)[source]

per_file mirrors xargs -n1: one invocation per target.

tests.test_tool_runner.test_resolve_default_args_empty_inventory_means_skip(tmp_path, monkeypatch)[source]

No matching file resolves to None, never to a pathless invocation.

This is the guard the whole feature exists for: a write-mode formatter handed zero paths walks the entire tree instead of doing nothing.

tests.test_tool_runner.test_run_tool_skips_a_tool_with_no_targets(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

An empty inventory means the tool is never spawned.

tests.test_tool_runner.test_run_tool_runs_once_per_file(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

A per-file tool is invoked once per target, installed once.

tests.test_tool_runner.test_run_tool_reports_the_first_failure_across_batches(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

Every target still runs, and no later success masks an earlier failure.

tests.test_tool_runner.test_run_tool_explicit_args_suppress_defaults(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

A caller-supplied argument means the caller drives the whole argv.

Without the all-or-nothing rule, biome’s format default would splice ahead of an explicit check, building a command neither one asked for.

tests.test_tool_runner.test_default_paths_names_a_real_inventory_group(spec)[source]

A tool’s declared target group exists and yields paths.

The field is a string because the inventory resolves against the working directory at call time, so a typo would otherwise surface as an AttributeError mid-run, in CI, after the tool was already installed.

tests.test_tool_runner.test_per_file_requires_default_paths(spec)[source]

per_file splits a target list, so it is meaningless without one.

tests.test_tool_runner.MESSY_PYPROJECT = '[project]\nname="orchard"\nversion="1.0"\n'

An unformatted pyproject.toml, the input a formatter is expected to rewrite.

tests.test_tool_runner.test_run_tool_reports_a_rewrite_status_that_rewrote_nothing(mock_ci, mock_run, mock_install, tmp_path, monkeypatch, caplog)[source]

A formatter claiming a rewrite it did not perform is reported as a crash.

This is how pyproject-fmt dies on a PanicException: exit code 1, the same one it uses for a successful reformat, and an untouched file. Callers have to tolerate that code, so without the file check the crash reads as a normal run and the autofix job goes green having formatted nothing.

tests.test_tool_runner.test_run_tool_passes_a_rewrite_status_that_rewrote_a_file(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

The same exit code is passed through untouched when a file did change.

tests.test_tool_runner.test_run_tool_leaves_other_tools_exit_codes_alone(mock_ci, mock_run, mock_install, tmp_path, monkeypatch)[source]

A tool declaring no rewrite status keeps reporting its own failures.

mdformat exits 1 on a genuine error, never to announce a rewrite, so the contradiction check must not reinterpret it.

tests.test_tool_runner.test_digest_targets_stays_out_of_git_directories(tmp_path, monkeypatch)[source]

A walk below a directory argument never enters a .git directory.

No tool rewrites a file there, and another process can. An index that an editor refreshes during the run would read as a file the tool rewrote. A .gitignore file is not in that directory, and stays in the walk.

tests.test_tool_runner.test_verify_via_write_path_propagates_a_crash(mock_run_tool, tmp_path, monkeypatch)[source]

A tool that dies on the copies is a failure, not a clean bill of health.

The copies come back unformatted, so comparing them to the originals finds no difference. Reporting that as “already formatted” would turn every crash into a passing verification.

tests.test_tool_runner.test_verify_via_write_path_accepts_the_working_directory(mock_run_tool, tmp_path, monkeypatch)[source]

A path resolving to the working directory verifies the default set.

The scratch root is created inside the working directory, so mirroring . under it makes the scratch its own copy destination and shutil.copytree dies on it as an existing path. It reached users as a bare FileExistsError: .repomatic-verify-<rand>, which reads as a repository problem rather than a rejected argument.

The collision happens while copying, before the tool runs, so a stubbed run_tool keeps the test off the network.

tests.test_tool_runner.test_verify_via_write_path_fails_when_nothing_is_copied(mock_run_tool, tool, extra_args, tmp_path, monkeypatch)[source]

A verification that copied no path measured nothing, so it cannot pass.

An exit code of 0 would read as a clean tree to any gate running it.

tests.test_tool_runner.test_verify_via_write_path_reports_a_path_it_cannot_copy(mock_run_tool, tmp_path, monkeypatch, caplog)[source]

A copy that fails is one logged error that names the path, not a traceback.

Nothing was measured, so the answer is the 2 of a call that copied no path, and the scratch directory does not outlive the call.

tests.test_tool_runner.test_run_rejects_an_unknown_tool_as_a_usage_error()[source]

An unknown tool name is refused at parse time, naming the registry.

It used to reach run_tool and raise a bare ValueError, so a typo printed a Python traceback instead of a usage error.

tests.test_tool_runner.test_run_offers_every_registered_tool()[source]

The argument’s choices are the registry, so neither can drift.