Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,42 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}


# --- NEW: Cache dependencies ---
- name: Get pip cache dir
id: pip-cache
run: |
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[test]" # Installs project and test dependencies
# Install black and ruff for linting/formatting
pip install ".[test]" black ruff

- name: Test with pytest
# --- NEW: Linter and Formatter Steps ---
- name: Check formatting with Black
run: |
black --check .
- name: Lint with Ruff
run: |
pytest
ruff check .

- name: Test with pytest and generate coverage report
run: |
# Add --cov to measure code coverage
pytest --cov=upgrade_tool --cov-report=xml

# --- NEW (Optional but Recommended): Upload coverage report ---
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }} # You need to set this secret in your repo
file: ./coverage.xml
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "ultimate-python-upgrader"
version = "1.3.0"
authors = [
{ name="Your Name", email="your@email.com" },
{ name="Sanyam Sanjay Sharma", email="infta2020+pypi@gmail.com" },
]
description = "An intelligent CLI tool to upgrade Python packages with dependency analysis and automatic rollback on failure."
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# This file makes the 'tests' directory a Python package.
# This file makes the 'tests' directory a Python package.
132 changes: 103 additions & 29 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,123 @@
import sys
import subprocess
from unittest.mock import patch

from typer.testing import CliRunner
from upgrade_tool.main import app
from upgrade_tool.main import app, upgrade_package, UpgradeStatus

# CliRunner is a utility from Typer for testing command-line applications
runner = CliRunner()


def test_app_shows_up_to_date_message(monkeypatch):
"""
Tests that the correct message is shown when no packages are outdated.
This test mocks `get_outdated_packages` to return an empty list.
"""
# Create a mock function that returns an empty list
def mock_get_outdated():
return []

# Use monkeypatch to replace the function *where it is used* in the main module
monkeypatch.setattr("upgrade_tool.main.get_outdated_packages", mock_get_outdated)

# Run the command
monkeypatch.setattr("upgrade_tool.main.get_outdated_packages", lambda: [])
result = runner.invoke(app)

# Assert that the exit code is 0 (success) and the correct message is in the output
assert result.exit_code == 0
assert "All packages are up to date!" in result.stdout


def test_app_exclusion_logic(monkeypatch):
"""
Tests the --exclude functionality.
This test mocks a fixed list of outdated packages to verify that the
exclusion logic works as intended.
"""
# Create a mock function that returns a predefined list of packages
def mock_get_outdated():
return [
{'name': 'requests', 'version': '2.25.0', 'latest_version': '2.28.0'},
{'name': 'numpy', 'version': '1.20.0', 'latest_version': '1.23.0'}
]
mock_outdated = [
{"name": "requests", "version": "2.25.0", "latest_version": "2.28.0"},
{"name": "numpy", "version": "1.20.0", "latest_version": "1.23.0"},
]
monkeypatch.setattr(
"upgrade_tool.main.get_outdated_packages", lambda: mock_outdated
)
result = runner.invoke(app, ["--exclude", "requests", "--dry-run"])
assert result.exit_code == 0
assert "requests" not in result.stdout
assert "numpy" in result.stdout
assert "1 packages selected" in result.stdout

# Use monkeypatch to replace the function *where it is used* in the main module
monkeypatch.setattr("upgrade_tool.main.get_outdated_packages", mock_get_outdated)

# Run the command with the --exclude flag and --dry-run to prevent actual upgrades
result = runner.invoke(app, ["--exclude", "requests", "--dry-run"])
# --- NEW: Comprehensive tests for the upgrade_package worker function ---

# Assertions
assert result.exit_code == 0
assert "requests" not in result.stdout # The excluded package should NOT be in the output table
assert "numpy" in result.stdout # The other package should be present
assert "1 packages selected" in result.stdout # The table caption should reflect the exclusion

@patch("upgrade_tool.main.subprocess.run")
def test_upgrade_package_success(mock_run):
"""Tests the successful upgrade path of the worker function."""
mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=""
)
pkg = {"name": "requests", "version": "2.25.0", "latest_version": "2.28.0"}

name, _, status, _, error = upgrade_package(pkg, no_rollback=False)

assert status == UpgradeStatus.SUCCESS
assert name == "requests"
assert error == ""
# Assert that pip install --upgrade was called
mock_run.assert_called_once_with(
[sys.executable, "-m", "pip", "install", "--upgrade", "requests"],
capture_output=True,
text=True,
check=True,
encoding="utf-8",
)


@patch("upgrade_tool.main.subprocess.run")
def test_upgrade_failure_with_successful_rollback(mock_run):
"""Tests that a failed upgrade triggers a successful rollback."""
# Simulate a failure on the first call (upgrade) and success on the second (rollback)
mock_run.side_effect = [
subprocess.CalledProcessError(returncode=1, cmd=[], stderr="Upgrade failed!"),
subprocess.CompletedProcess(
args=[], returncode=0, stdout="", stderr=""
), # This is the rollback call
]
pkg = {"name": "requests", "version": "2.25.0", "latest_version": "2.28.0"}

_, _, status, _, error = upgrade_package(pkg, no_rollback=False)

assert status == UpgradeStatus.ROLLBACK_SUCCESS
assert "Upgrade failed!" in error
assert mock_run.call_count == 2
# Check the second call was the rollback
rollback_call_args = mock_run.call_args_list[1].args[0]
assert "--force-reinstall" in rollback_call_args
assert "requests==2.25.0" in rollback_call_args


@patch("upgrade_tool.main.subprocess.run")
def test_upgrade_failure_with_failed_rollback(mock_run):
"""Tests a critical failure where both upgrade and rollback fail."""
# Simulate failure on both calls
mock_run.side_effect = [
subprocess.CalledProcessError(returncode=1, cmd=[], stderr="Upgrade failed!"),
subprocess.CalledProcessError(
returncode=1, cmd=[], stderr="Rollback also failed!"
),
]
pkg = {"name": "requests", "version": "2.25.0", "latest_version": "2.28.0"}

_, _, status, _, error = upgrade_package(pkg, no_rollback=False)

assert status == UpgradeStatus.ROLLBACK_FAILED
assert "Upgrade Error: Upgrade failed!" in error
assert "Rollback Error: Rollback also failed!" in error
assert mock_run.call_count == 2


@patch("upgrade_tool.main.subprocess.run")
def test_upgrade_failure_with_no_rollback_enabled(mock_run):
"""Tests that a failed upgrade does not attempt rollback when disabled."""
# Simulate failure on the first call
mock_run.side_effect = subprocess.CalledProcessError(
returncode=1, cmd=[], stderr="Upgrade failed!"
)
pkg = {"name": "requests", "version": "2.25.0", "latest_version": "2.28.0"}

_, _, status, _, error = upgrade_package(pkg, no_rollback=True)

assert status == UpgradeStatus.UPGRADE_FAILED
assert "Upgrade failed!" in error
# With no_rollback=True, only one call to subprocess.run should be made
mock_run.assert_called_once()
2 changes: 1 addition & 1 deletion upgrade_tool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Ultimate Python Upgrader (`py-upgrade`)

An intelligent, feature-rich CLI tool to manage and upgrade Python packages.
"""
"""
Loading