From d990b7920cfb293dd55708dd9f1c1578b16f5f39 Mon Sep 17 00:00:00 2001 From: Zanie Date: Thu, 14 Dec 2023 11:30:36 -0600 Subject: [PATCH] Improve publish test coverage --- src/packse/publish.py | 8 +- tests/__snapshots__/test_publish.ambr | 86 +++++++++++++- tests/common.py | 4 + tests/test_publish.py | 159 +++++++++++++++++++++++--- 4 files changed, 236 insertions(+), 21 deletions(-) diff --git a/src/packse/publish.py b/src/packse/publish.py index 75cb08be..89d6fb1b 100644 --- a/src/packse/publish.py +++ b/src/packse/publish.py @@ -87,7 +87,11 @@ def publish_package_distribution(target: Path, dry_run: bool) -> None: start_time = time.time() try: - output = subprocess.check_output(command, stderr=subprocess.STDOUT) + import os + + output = subprocess.check_output( + command, stderr=subprocess.STDOUT, env=os.environ + ) except subprocess.CalledProcessError as exc: output = exc.output.decode() if "File already exists" in output: @@ -95,7 +99,7 @@ def publish_package_distribution(target: Path, dry_run: bool) -> None: if "HTTPError: 429 Too Many Requests" in output: raise PublishRateLimit(target.name) raise PublishToolError( - f"Publishing {target} with twine failed", + f"Publishing {target.name} with twine failed", output, ) else: diff --git a/tests/__snapshots__/test_publish.ambr b/tests/__snapshots__/test_publish.ambr index fa2c68f4..b94a6912 100644 --- a/tests/__snapshots__/test_publish.ambr +++ b/tests/__snapshots__/test_publish.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_publish_example +# name: test_publish_example_dry_run dict({ 'exit_code': 0, 'stderr': ''' @@ -13,12 +13,86 @@ ''', 'stdout': ''' - Would execute: twine upload -r testpypi [PWD]/dist/example-0611cb74/example_0611cb74-0.0.0.tar.gz - Would execute: twine upload -r testpypi [PWD]/dist/example-0611cb74/example_0611cb74_a-1.0.0-py3-none-any.whl - Would execute: twine upload -r testpypi [PWD]/dist/example-0611cb74/example_0611cb74_a-1.0.0.tar.gz - Would execute: twine upload -r testpypi [PWD]/dist/example-0611cb74/example_0611cb74_b-1.0.0-py3-none-any.whl - Would execute: twine upload -r testpypi [PWD]/dist/example-0611cb74/example_0611cb74_b-1.0.0.tar.gz + Would execute: twine upload -r testpypi /private/var/folders/bc/qlsk3t6x7c9fhhbvvcg68k9c0000gp/T/tmp8bvy1hji/dist/example-0611cb74/example_0611cb74-0.0.0.tar.gz + Would execute: twine upload -r testpypi /private/var/folders/bc/qlsk3t6x7c9fhhbvvcg68k9c0000gp/T/tmp8bvy1hji/dist/example-0611cb74/example_0611cb74_a-1.0.0-py3-none-any.whl + Would execute: twine upload -r testpypi /private/var/folders/bc/qlsk3t6x7c9fhhbvvcg68k9c0000gp/T/tmp8bvy1hji/dist/example-0611cb74/example_0611cb74_a-1.0.0.tar.gz + Would execute: twine upload -r testpypi /private/var/folders/bc/qlsk3t6x7c9fhhbvvcg68k9c0000gp/T/tmp8bvy1hji/dist/example-0611cb74/example_0611cb74_b-1.0.0-py3-none-any.whl + Would execute: twine upload -r testpypi /private/var/folders/bc/qlsk3t6x7c9fhhbvvcg68k9c0000gp/T/tmp8bvy1hji/dist/example-0611cb74/example_0611cb74_b-1.0.0.tar.gz ''', }) # --- +# name: test_publish_example_twine_fails_with_already_exists + dict({ + 'exit_code': 1, + 'stderr': ''' + INFO:packse.publish:Publishing 1 target... + INFO:packse.publish:Publishing 'example-0611cb74'... + Publish for 'example_0611cb74-0.0.0.tar.gz' already exists + + ''', + 'stdout': '', + }) +# --- +# name: test_publish_example_twine_fails_with_rate_limit + dict({ + 'exit_code': 1, + 'stderr': ''' + INFO:packse.publish:Publishing 1 target... + INFO:packse.publish:Publishing 'example-0611cb74'... + Publish of 'example_0611cb74-0.0.0.tar.gz' failed due to rate limits + + ''', + 'stdout': '', + }) +# --- +# name: test_publish_example_twine_fails_with_unknown_error + dict({ + 'exit_code': 1, + 'stderr': ''' + INFO:packse.publish:Publishing 1 target... + INFO:packse.publish:Publishing 'example-0611cb74'... + Publishing example_0611cb74-0.0.0.tar.gz with twine failed: + + + + ''', + 'stdout': '', + }) +# --- +# name: test_publish_example_twine_succeeds + dict({ + 'exit_code': 0, + 'stderr': ''' + INFO:packse.publish:Publishing 1 target... + INFO:packse.publish:Publishing 'example-0611cb74'... + DEBUG:packse.publish:Published example_0611cb74-0.0.0.tar.gz in [TIME]: + + + + INFO:packse.publish:Published 'example_0611cb74-0.0.0.tar.gz' + DEBUG:packse.publish:Published example_0611cb74_a-1.0.0-py3-none-any.whl in [TIME]: + + + + INFO:packse.publish:Published 'example_0611cb74_a-1.0.0-py3-none-any.whl' + DEBUG:packse.publish:Published example_0611cb74_a-1.0.0.tar.gz in [TIME]: + + + + INFO:packse.publish:Published 'example_0611cb74_a-1.0.0.tar.gz' + DEBUG:packse.publish:Published example_0611cb74_b-1.0.0-py3-none-any.whl in [TIME]: + + + + INFO:packse.publish:Published 'example_0611cb74_b-1.0.0-py3-none-any.whl' + DEBUG:packse.publish:Published example_0611cb74_b-1.0.0.tar.gz in [TIME]: + + + + INFO:packse.publish:Published 'example_0611cb74_b-1.0.0.tar.gz' + + ''', + 'stdout': '', + }) +# --- diff --git a/tests/common.py b/tests/common.py index 08b6b2f2..a880a8d1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -24,6 +24,7 @@ def snapshot_command( snapshot_filesystem: bool = False, stderr: bool = True, stdout: bool = True, + extra_filters: list[tuple[str, str]] | None = None, ) -> dict: # By default, filter out absolute references to the working directory filters = [ @@ -34,12 +35,15 @@ def snapshot_command( "[TIME]", ), ] + if extra_filters: + filters += extra_filters process = subprocess.run( ["packse"] + command, cwd=working_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=os.environ, ) result = { "exit_code": process.returncode, diff --git a/tests/test_publish.py b/tests/test_publish.py index c6a1d26b..2dd4e792 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -3,23 +3,156 @@ from packse import __development_base_path__ -from .common import snapshot_command +from .common import snapshot_command, tmpchdir +import tempfile +import shutil +import packse.publish +import pytest +import stat +import re +import subprocess +from typing import Generator +from unittest.mock import MagicMock -def test_publish_example(snapshot, tmpcwd: Path): +@pytest.fixture(scope="module") +def scenario_dist() -> Generator[Path, None, None]: target = __development_base_path__ / "scenarios" / "example.json" - # Build first - # TODO(zanieb): Since we're doing a dry run consider just constructing some fake files? - subprocess.check_call( - ["packse", "build", str(target)], - cwd=tmpcwd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + with tempfile.TemporaryDirectory() as tmpdir: + subprocess.check_call( + ["packse", "build", str(target)], + cwd=tmpdir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + dists = list((Path(tmpdir) / "dist").iterdir()) + assert len(dists) == 1 + dist = dists[0] + + yield dist + + +class MockBinary: + def __init__(self, path: Path) -> None: + self.path = path + self.callback = None + self._update_bin("") + + def _prepare_text(self, text: str = None): + if not text: + return "" + # Escape single quotes + return text.replace("'", "\\'") + + def _update_bin(self, content: str): + self.path.write_text("#!/usr/bin/env sh\n\n" + content + "\n") + self.path.chmod(self.path.stat().st_mode | stat.S_IEXEC) + + def set_success(self, text: str | None = None): + text = self._prepare_text(text) + self._update_bin(f"echo '{text}'") + + def set_error(self, text: str | None = None): + text = self._prepare_text(text) + self._update_bin(f"echo '{text}'; exit 1") + + +@pytest.fixture +def mock_twine(monkeypatch: pytest.MonkeyPatch) -> Generator[MockBinary, None, None]: + # Create a temp directory to register as a bin + with tempfile.TemporaryDirectory() as tmpdir: + mock = MockBinary(Path(tmpdir) / "twine") + mock.set_success() + + # Add to the path + monkeypatch.setenv("PATH", tmpdir, prepend=":") + assert shutil.which("twine").startswith(tmpdir) + + yield mock + + +def test_publish_example_dry_run(snapshot, scenario_dist: Path): + assert snapshot_command(["publish", "--dry-run", scenario_dist]) == snapshot + + +def test_publish_example_twine_succeeds( + snapshot, scenario_dist: Path, mock_twine: MockBinary +): + mock_twine.set_success("") + + assert ( + snapshot_command( + ["publish", scenario_dist, "-v"], + extra_filters=[(re.escape(str(scenario_dist)), "[DISTDIR]")], + ) + == snapshot + ) + + +def test_publish_example_twine_fails_with_unknown_error( + snapshot, scenario_dist: Path, mock_twine: MockBinary +): + mock_twine.set_error("") + + assert ( + snapshot_command( + ["publish", scenario_dist, "-v"], + extra_filters=[(re.escape(str(scenario_dist)), "[DISTDIR]")], + ) + == snapshot ) - dists = list((tmpcwd / "dist").iterdir()) - assert len(dists) == 1 - dist = dists[0] - assert snapshot_command(["publish", "--dry-run", dist]) == snapshot +def test_publish_example_twine_fails_with_rate_limit( + snapshot, scenario_dist: Path, mock_twine: MockBinary +): + mock_twine.set_error( + """ +Uploading distributions to https://test.pypi.org/legacy/ +Uploading +requires_transitive_incompatible_with_root_version_5c1b7dc1_c-1.0.0-py3-none-any +.whl +25l + 0% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/4.1 kB • --:-- • ? +100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 kB • 00:00 • ? +100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 kB • 00:00 • ? +100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 kB • 00:00 • ? +100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 kB • 00:00 • ? +25hWARNING Error during upload. Retry with the --verbose option for more details. +ERROR HTTPError: 429 Too Many Requests from https://test.pypi.org/legacy/ + Too many new projects created + """ + ) + + assert ( + snapshot_command( + ["publish", scenario_dist, "-v"], + extra_filters=[(re.escape(str(scenario_dist)), "[DISTDIR]")], + ) + == snapshot + ) + + +def test_publish_example_twine_fails_with_already_exists( + snapshot, scenario_dist: Path, mock_twine: MockBinary +): + mock_twine.set_error( + """ +Uploading distributions to https://test.pypi.org/legacy/ +Uploading example_9e723676_a-1.0.0.tar.gz +100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 kB • 00:00 • ? +WARNING Error during upload. Retry with the --verbose option for more details. +ERROR HTTPError: 400 Bad Request from https://test.pypi.org/legacy/ + File already exists. See https://test.pypi.org/help/#file-name-reuse for more information. + """ + ) + + assert ( + snapshot_command( + ["publish", scenario_dist, "-v"], + extra_filters=[(re.escape(str(scenario_dist)), "[DISTDIR]")], + ) + == snapshot + )