Skip to content

Enhance the testsuite with new custom CLI Runner - #2468

Open
sirosen wants to merge 5 commits into
pypa:mainfrom
sirosen:improve-test-cli-runner
Open

sirosen wants to merge 5 commits into
pypa:mainfrom
sirosen:improve-test-cli-runner

Conversation

@sirosen

@sirosen sirosen commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

This changeset touches many lines throughout the testsuite, but all in service of two primary changes:

  1. Introduce PipToolsCliRunner, a wrapper over click's CliRunner.

  2. Convert usages of open(...) for file IO to use pathlib.

The CLiRunner wrapper provides nicer default behaviors, named functions for pip-compile and pip-sync, and automatically asserts that the exit code of a command is 0 (unless overridden with another code to assert). For very simple cases, the CLI runner accepts a string rather than a list of arguments, which it will pass through shlex.split().

In concert, these enhancements allow usage like

 result = runner.invoke(
      compile_command, ["-h"], catch_exceptions=False
 )
 assert result.exit_code == 0, f"{result.stderr=}, {result.stdout=}"

to be rewritten as

 runner.pip_compile("-h")

These simplifications accrue accross the entire testsuite for a significant cleanup and tightening of behaviors.

The new CLI runner's enhanced behaviors reveals several flaws in specific tests, which need tweaks and refactors to pass. Tests which have non-zero exit codes for CLI behaviors now have to account for this. In a couple of specific cases, tests raised exceptions which were captured by the runner, but not properly handled. This obscured breakage caused by improper patching/mocking. Some tests are newly sensitive to the legacy resolver vs the modern, backtracking one, because they exhibit different exit codes.

Contributor checklist
  • Included tests for the changes.
  • A change note is created in changelog.d/ (see changelog.d/README.md
    for instructions) or the PR text says "no changelog needed".
Maintainer checklist
  • If no changelog is needed, apply the bot:chronographer:skip label.
  • Assign the PR to an existing or new milestone for the target version
    (following Semantic Versioning).

This changeset touches many lines throughout the testsuite, but all in
service of two primary changes:

1.  Introduce PipToolsCliRunner, a wrapper over click's CliRunner.

2.  Convert usages of `open(...)` for file IO to use pathlib.

The CLiRunner wrapper provides nicer default behaviors, named functions
for `pip-compile` and `pip-sync`, and automatically asserts that the
exit code of a command is 0 (unless overridden with another code to
assert). For very simple cases, the CLI runner accepts a string rather
than a list of arguments, which it will pass through `shlex.split()`.

In concert, these enhancements allow usage like

     result = runner.invoke(
          compile_command, ["-h"], catch_exceptions=False
     )
     assert result.exit_code == 0, f"{result.stderr=}, {result.stdout=}"

to be rewritten as

     runner.pip_compile("-h")

These simplifications accrue accross the entire testsuite for a
significant cleanup and tightening of behaviors.

The new CLI runner's enhanced behaviors reveals several flaws in specific
tests, which need tweaks and refactors to pass. Tests which have
non-zero exit codes for CLI behaviors now have to account for this. In
a couple of specific cases, tests raised exceptions which were captured
by the runner, but not properly handled. This obscured breakage caused by
improper patching/mocking. Some tests are newly sensitive to the legacy
resolver vs the modern, backtracking one, because they exhibit different
exit codes.
@sirosen sirosen added this to the 7.6.2 milestone Aug 23, 2026
@sirosen
sirosen requested a review from a team August 23, 2026 07:00
@pyba-chronographer pyba-chronographer Bot added the bot:chronographer:provided A mark meaning that a new change log entry is present within the patch. label Aug 23, 2026
- Do not test `--build-option` passed to `pip-compile` -- it is not a
  valid install option and fails on lower pip versions.

- Use pathlib path objects for path constants, fixing an improper path
  join.
After the CLI Runner refactor, which required fixing other tests, no test
was exercising an error raising condition in the legacy resolver wrapper.
Dedicated unit tests restore coverage for the resolver and the
pip-compile main function.
@sirosen
sirosen force-pushed the improve-test-cli-runner branch from 9bbcac9 to 06cbe80 Compare August 24, 2026 03:58
@sirosen sirosen modified the milestones: 7.6.2, 7.7.0 Oct 5, 2026
Comment thread tests/conftest.py Outdated


@pytest.fixture
def runner(tmp_path_cwd):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have typing and docstrings for new things plz?

Comment thread tests/conftest.py
def invoke(
self,
cli_entry_point: click.BaseCommand,
args: _c.Sequence[str] | str | None = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate pos-only from kwargs?

Suggested change
args: _c.Sequence[str] | str | None = None,
args: _c.Sequence[str] | str | None = None,
/,
*,

Per review, make CLI runner method args clearly positional-only vs
keyword-only, and add an annotation to the new runner fixture.

Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <578543+webknjaz@users.noreply.github.com>
Comment on lines +10 to +11
repository, depcache, tmp_path
):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing?

from piptools.resolver import LegacyResolver


def test_legacy_resolver_checks_that_deprecated_feature_is_enabled(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it's not two tests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. 😁
I'll split it.


# having confirmed those args work, try with a repo object which does not enable
# the legacy resolver and confirm that it errors
repository.options.deprecated_features_enabled = []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not set an explicit value for the test above too? seems a bit fragile w/o it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository fixture looks like so:

@pytest.fixture
def repository():
    return FakeRepository(
        options=FakeOptions(deprecated_features_enabled=["legacy-resolver"])
    )

So if we make this testing more explicit, I feel like we're stepping on our own toes a bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I altered it to a parametrized test, and in the course of doing that, setting the value explicitly (even though it's redundant with the fixture) made a kind of sense.

Add missing type annotations and split the two cases being checked into
separate parametrized cases. This also naturally makes the positive case
more explicit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided A mark meaning that a new change log entry is present within the patch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants