debian-todo/tests/test_update_pkg.py

933 lines
37 KiB
Python

"""Tests for update-pkg command functions."""
import datetime
import json
import os
import subprocess
import urllib.error
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import debian_todo
class TestRunCommand:
"""Tests for run_command helper function."""
def test_successful_command(self, tmp_path):
"""Test running a successful command."""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
result = debian_todo.run_command(
["echo", "test"], tmp_path, "Test command"
)
assert result.returncode == 0
def test_command_with_capture_output(self, tmp_path):
"""Test running a command with output capture."""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
returncode=0, stdout="output", stderr=""
)
result = debian_todo.run_command(
["echo", "test"], tmp_path, "Test command", capture_output=True
)
assert result.stdout == "output"
mock_run.assert_called_once_with(
["echo", "test"], cwd=tmp_path, capture_output=True, text=True
)
def test_command_failure_with_check(self, tmp_path):
"""Test command failure raises ExternalCommandError when check=True."""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")
with pytest.raises(debian_todo.ExternalCommandError) as exc_info:
debian_todo.run_command(
["false"], tmp_path, "Test command", check=True
)
assert exc_info.value.returncode == 1
assert "false" in exc_info.value.command
assert "Test command failed" in str(exc_info.value)
def test_command_failure_without_check(self, tmp_path):
"""Test command failure returns result when check=False."""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")
result = debian_todo.run_command(
["false"], tmp_path, "Test command", check=False
)
assert result.returncode == 1
def test_missing_command(self, tmp_path):
"""Test missing command raises MissingToolError."""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.side_effect = FileNotFoundError("command not found")
with pytest.raises(debian_todo.MissingToolError) as exc_info:
debian_todo.run_command(
["nonexistent"], tmp_path, "Test command"
)
assert "nonexistent" in str(exc_info.value)
class TestCheckRequiredTools:
"""Tests for check_required_tools function."""
def test_all_tools_available(self):
"""Test when all required tools are available."""
with patch("debian_todo.shutil.which") as mock_which:
mock_which.return_value = "/usr/bin/tool"
debian_todo.check_required_tools()
assert mock_which.call_count == 3
def test_missing_salsa(self):
"""Test when salsa is missing."""
with patch("debian_todo.shutil.which") as mock_which:
def which_side_effect(tool):
return None if tool == "salsa" else "/usr/bin/tool"
mock_which.side_effect = which_side_effect
with pytest.raises(debian_todo.MissingToolError) as exc_info:
debian_todo.check_required_tools()
assert "salsa" in str(exc_info.value)
def test_missing_gbp(self):
"""Test when gbp is missing."""
with patch("debian_todo.shutil.which") as mock_which:
def which_side_effect(tool):
return None if tool == "gbp" else "/usr/bin/tool"
mock_which.side_effect = which_side_effect
with pytest.raises(debian_todo.MissingToolError) as exc_info:
debian_todo.check_required_tools()
assert "gbp" in str(exc_info.value)
def test_missing_multiple_tools(self):
"""Test when multiple tools are missing."""
with patch("debian_todo.shutil.which") as mock_which:
mock_which.return_value = None
with pytest.raises(debian_todo.MissingToolError) as exc_info:
debian_todo.check_required_tools()
error_msg = str(exc_info.value)
assert "salsa" in error_msg
assert "gbp" in error_msg
assert "dch" in error_msg
class TestFetchTodoListErrors:
"""Tests for fetch_todo_list error handling."""
def test_http_error(self):
"""Test HTTP error handling."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.HTTPError(
debian_todo.TODO_URL, 404, "Not Found", {}, None
)
with pytest.raises(debian_todo.PackageUpdateError) as exc_info:
debian_todo.fetch_todo_list()
assert "HTTP error 404" in str(exc_info.value)
assert "Not Found" in str(exc_info.value)
def test_network_error(self):
"""Test network error handling."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
with pytest.raises(debian_todo.PackageUpdateError) as exc_info:
debian_todo.fetch_todo_list()
assert "Network error" in str(exc_info.value)
assert "Connection refused" in str(exc_info.value)
def test_timeout_error(self):
"""Test timeout error handling."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_urlopen.side_effect = TimeoutError("Request timed out")
with pytest.raises(debian_todo.PackageUpdateError) as exc_info:
debian_todo.fetch_todo_list(timeout=10)
assert "Timeout after 10s" in str(exc_info.value)
def test_invalid_json(self):
"""Test invalid JSON response handling."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_response = MagicMock()
mock_response.read.return_value = b"not valid json {"
mock_response.__enter__ = lambda self: self
mock_response.__exit__ = lambda self, *args: None
mock_urlopen.return_value = mock_response
with pytest.raises(debian_todo.PackageUpdateError) as exc_info:
debian_todo.fetch_todo_list()
assert "Invalid JSON" in str(exc_info.value)
assert "not valid json {" in str(exc_info.value)
def test_udd_high_load_html_response(self):
"""Test known UDD overload HTML response handling."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_response = MagicMock()
mock_response.read.return_value = (
b"<p><b>Current system load (26.1) is too high. "
b"Please retry later!</b></p>\n"
)
mock_response.__enter__ = lambda self: self
mock_response.__exit__ = lambda self, *args: None
mock_urlopen.return_value = mock_response
with pytest.raises(debian_todo.PackageUpdateError) as exc_info:
debian_todo.fetch_todo_list()
assert "temporary overload page" in str(exc_info.value)
assert "Please retry later" in str(exc_info.value)
def test_successful_fetch(self):
"""Test successful fetch."""
with patch("debian_todo.urlopen") as mock_urlopen:
mock_response = MagicMock()
mock_response.read.return_value = b'[{"source": "pkg1"}]'
mock_response.__enter__ = lambda self: self
mock_response.__exit__ = lambda self, *args: None
mock_urlopen.return_value = mock_response
result = debian_todo.fetch_todo_list()
assert result == [{"source": "pkg1"}]
class TestTeamSlugToDisplayName:
"""Tests for team_slug_to_display_name function."""
def test_homeassistant_team(self):
assert debian_todo.team_slug_to_display_name("homeassistant-team") == "HA"
def test_python_team(self):
assert debian_todo.team_slug_to_display_name("python-team") == "python"
def test_openstack_team(self):
assert debian_todo.team_slug_to_display_name("openstack-team") == "openstack"
def test_other_team(self):
assert debian_todo.team_slug_to_display_name("rust-team") == "rust"
def test_no_team_suffix(self):
assert debian_todo.team_slug_to_display_name("username") == "username"
class TestHasUncommittedChanges:
"""Tests for has_uncommitted_changes function."""
def test_no_changes(self, tmp_path):
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="", returncode=0)
assert debian_todo.has_uncommitted_changes(tmp_path) is False
mock_run.assert_called_once_with(
["git", "status", "--porcelain"],
cwd=tmp_path,
capture_output=True,
text=True,
)
def test_has_changes(self, tmp_path):
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout=" M file.txt\n", returncode=0)
assert debian_todo.has_uncommitted_changes(tmp_path) is True
def test_untracked_files(self, tmp_path):
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="?? newfile.txt\n", returncode=0)
assert debian_todo.has_uncommitted_changes(tmp_path) is True
def test_whitespace_only_output(self, tmp_path):
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout=" \n", returncode=0)
assert debian_todo.has_uncommitted_changes(tmp_path) is False
class TestExtractUpstreamVersionFromGitLog:
"""Tests for extract_upstream_version_from_git_log function."""
def test_extracts_version(self, tmp_path):
git_log_output = """commit abc123
Author: Someone <someone@example.com>
Date: Mon Jan 1 12:00:00 2024 +0000
Update upstream source from tag 'upstream/1.2.3'
"""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout=git_log_output, returncode=0
)
result = debian_todo.extract_upstream_version_from_git_log(tmp_path)
assert result == "1.2.3"
def test_extracts_complex_version(self, tmp_path):
git_log_output = """commit abc123
Author: Someone <someone@example.com>
Date: Mon Jan 1 12:00:00 2024 +0000
Update upstream source from tag 'upstream/2024.1.0rc1'
"""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout=git_log_output, returncode=0
)
result = debian_todo.extract_upstream_version_from_git_log(tmp_path)
assert result == "2024.1.0rc1"
def test_no_upstream_tag(self, tmp_path):
git_log_output = """commit abc123
Author: Someone <someone@example.com>
Date: Mon Jan 1 12:00:00 2024 +0000
Fixed a bug
"""
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
stdout=git_log_output, returncode=0
)
result = debian_todo.extract_upstream_version_from_git_log(tmp_path)
assert result is None
def test_git_log_fails(self, tmp_path):
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="", returncode=1)
result = debian_todo.extract_upstream_version_from_git_log(tmp_path)
assert result is None
class TestValidatePackageInfo:
"""Tests for validate_package_info function."""
def test_valid_package(self, monkeypatch):
"""Test validating a package that exists."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
result = debian_todo.validate_package_info("mypkg")
assert result == {"vcs_git": "python-team", "uploaders": ""}
def test_package_not_found(self, monkeypatch):
"""Test package not in source info map."""
monkeypatch.setattr(debian_todo, "load_source_info_map", lambda: {})
with pytest.raises(debian_todo.PackageNotFoundError) as exc_info:
debian_todo.validate_package_info("nonexistent")
assert "nonexistent" in str(exc_info.value)
def test_package_no_vcs_git(self, monkeypatch):
"""Test package with no vcs_git field."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "", "uploaders": ""}},
)
with pytest.raises(debian_todo.PackageNotFoundError) as exc_info:
debian_todo.validate_package_info("mypkg")
assert "mypkg" in str(exc_info.value)
class TestResolvePackageDirectories:
"""Tests for resolve_package_directories function."""
def test_python_team_package(self, tmp_path, monkeypatch):
"""Test resolving directories for python-team package."""
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
source_info = {"vcs_git": "python-team", "uploaders": ""}
team_dir, pkg_dir, repo_dir = debian_todo.resolve_package_directories(
"mypkg", source_info
)
assert team_dir == tmp_path / "python"
assert pkg_dir == tmp_path / "python" / "mypkg"
assert repo_dir == tmp_path / "python" / "mypkg" / "mypkg"
def test_homeassistant_team_package(self, tmp_path, monkeypatch):
"""Test resolving directories for homeassistant-team package."""
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
source_info = {"vcs_git": "homeassistant-team", "uploaders": ""}
team_dir, pkg_dir, repo_dir = debian_todo.resolve_package_directories(
"ha-pkg", source_info
)
assert team_dir == tmp_path / "ha"
assert pkg_dir == tmp_path / "ha" / "ha-pkg"
assert repo_dir == tmp_path / "ha" / "ha-pkg" / "ha-pkg"
class TestEnsurePackageCheckout:
"""Tests for ensure_package_checkout function."""
def test_successful_checkout(self, tmp_path):
"""Test successful package checkout."""
pkg_dir = tmp_path / "python" / "mypkg"
source_info = {"vcs_git": "python-team", "uploaders": ""}
with patch("debian_todo.run_command") as mock_run:
debian_todo.ensure_package_checkout("mypkg", source_info, pkg_dir)
assert pkg_dir.exists()
mock_run.assert_called_once_with(
["salsa", "checkout", "python-team/deps/mypkg"],
pkg_dir,
"Checkout python-team/deps/mypkg",
)
def test_checkout_with_existing_parent(self, tmp_path):
"""Test checkout when parent directory already exists."""
pkg_dir = tmp_path / "python" / "mypkg"
pkg_dir.mkdir(parents=True)
source_info = {"vcs_git": "python-team", "uploaders": ""}
with patch("debian_todo.run_command") as mock_run:
debian_todo.ensure_package_checkout("mypkg", source_info, pkg_dir)
assert pkg_dir.exists()
class TestValidateRepositoryState:
"""Tests for validate_repository_state function."""
def test_clean_repository(self, tmp_path):
"""Test validating a clean repository."""
with patch("debian_todo.has_uncommitted_changes") as mock_has_changes:
mock_has_changes.return_value = False
debian_todo.validate_repository_state(tmp_path)
def test_dirty_repository(self, tmp_path):
"""Test validating a repository with uncommitted changes."""
with patch("debian_todo.has_uncommitted_changes") as mock_has_changes:
mock_has_changes.return_value = True
with pytest.raises(debian_todo.RepositoryStateError) as exc_info:
debian_todo.validate_repository_state(tmp_path)
assert "uncommitted changes" in str(exc_info.value)
class TestRunGbpPqWorkflow:
"""Tests for run_gbp_pq_workflow function."""
def test_successful_workflow(self, tmp_path):
"""Test successful gbp pq import and switch."""
with patch("debian_todo.run_command") as mock_run:
debian_todo.run_gbp_pq_workflow(tmp_path)
assert mock_run.call_count == 2
mock_run.assert_any_call(
["gbp", "pq", "import"], tmp_path, "Import patch queue"
)
mock_run.assert_any_call(
["gbp", "pq", "switch"], tmp_path, "Switch to patch branch"
)
def test_import_fails(self, tmp_path):
"""Test when gbp pq import fails."""
with patch("debian_todo.run_command") as mock_run:
mock_run.side_effect = debian_todo.ExternalCommandError(
"gbp pq import", 1, "Import failed"
)
with pytest.raises(debian_todo.ExternalCommandError):
debian_todo.run_gbp_pq_workflow(tmp_path)
class TestImportUpstreamVersion:
"""Tests for import_upstream_version function."""
def test_successful_import(self, tmp_path, capsys):
"""Test successful upstream import."""
with patch("debian_todo.run_command") as mock_run:
debian_todo.import_upstream_version(tmp_path)
mock_run.assert_called_once_with(
["gbp", "import-orig", "--uscan", "--pristine-tar", "--no-interactive"],
tmp_path,
"Import upstream version",
)
captured = capsys.readouterr()
assert "Importing new upstream version" in captured.out
def test_import_fails(self, tmp_path):
"""Test when upstream import fails."""
with patch("debian_todo.run_command") as mock_run:
mock_run.side_effect = debian_todo.ExternalCommandError(
"gbp import-orig", 1, "Import failed"
)
with pytest.raises(debian_todo.ExternalCommandError):
debian_todo.import_upstream_version(tmp_path)
class TestRunPackageUpdates:
"""Tests for run_package_updates function."""
def test_runs_all_updates(self, tmp_path):
"""Test that all update functions are called."""
with patch("debian_todo.update_debian_control") as mock_control:
with patch("debian_todo.update_debian_copyright_year") as mock_copyright:
with patch("debian_todo.update_debian_watch") as mock_watch:
with patch("debian_todo.add_salsa_ci") as mock_salsa:
debian_todo.run_package_updates(tmp_path)
mock_control.assert_called_once_with(tmp_path)
mock_copyright.assert_called_once_with(tmp_path)
mock_watch.assert_called_once_with(tmp_path)
mock_salsa.assert_called_once_with(tmp_path)
class TestUpdateDebianControl:
"""Tests for update_debian_control function."""
def test_removes_priority_optional(self, tmp_path):
"""Test removing obsolete Priority: optional."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
control_path = debian_dir / "control"
control_path.write_text("Source: mypkg\nPriority: optional\n")
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_control(tmp_path)
mock_run.assert_called_once()
assert "Priority: optional" in mock_run.call_args[0][0][2]
# Verify Priority: optional was removed
content = control_path.read_text()
assert "Priority: optional" not in content
def test_updates_standards_version(self, tmp_path):
"""Test updating Standards-Version."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
control_path = debian_dir / "control"
control_path.write_text("Source: mypkg\nStandards-Version: 4.6.0\n")
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_control(tmp_path)
mock_run.assert_called_once()
assert "Standards-Version" in mock_run.call_args[0][0][2]
# Verify Standards-Version was updated
content = control_path.read_text()
assert "Standards-Version: 4.7.3" in content
def test_no_changes_needed(self, tmp_path):
"""Test when no changes are needed."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
control_path = debian_dir / "control"
control_path.write_text("Source: mypkg\nStandards-Version: 4.7.3\n")
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_control(tmp_path)
mock_run.assert_not_called()
def test_missing_control_file(self, tmp_path):
"""Test when debian/control doesn't exist."""
debian_todo.update_debian_control(tmp_path) # Should not raise
class TestUpdateDebianCopyrightYear:
"""Tests for update_debian_copyright_year function."""
def test_updates_single_year(self, tmp_path):
"""Test updating a single year to a range."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
copyright_path = debian_dir / "copyright"
current_year = datetime.date.today().year
last_year = current_year - 1
copyright_path.write_text(
f"Copyright: {last_year} Edward Betts <edward@4angle.com>\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_copyright_year(tmp_path)
mock_run.assert_called_once()
# Verify year was updated
content = copyright_path.read_text()
assert f"{last_year}-{current_year}" in content
def test_updates_year_range(self, tmp_path):
"""Test extending a year range."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
copyright_path = debian_dir / "copyright"
current_year = datetime.date.today().year
copyright_path.write_text(
f"Copyright: 2020-{current_year - 2} Edward Betts <edward@4angle.com>\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_copyright_year(tmp_path)
mock_run.assert_called_once()
# Verify year range was updated
content = copyright_path.read_text()
assert f"2020-{current_year}" in content
def test_no_update_needed(self, tmp_path):
"""Test when copyright year is already current."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
copyright_path = debian_dir / "copyright"
current_year = datetime.date.today().year
copyright_path.write_text(
f"Copyright: {current_year} Edward Betts <edward@4angle.com>\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_copyright_year(tmp_path)
mock_run.assert_not_called()
def test_missing_copyright_file(self, tmp_path):
"""Test when debian/copyright doesn't exist."""
debian_todo.update_debian_copyright_year(tmp_path) # Should not raise
class TestUpdateDebianWatch:
"""Tests for update_debian_watch function."""
def test_upgrades_github_watch(self, tmp_path):
"""Test upgrading a GitHub watch file."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
watch_path = debian_dir / "watch"
watch_path.write_text(
"version=4\n"
"https://github.com/owner/repo/releases .*/v?([\\d.]+)\\.tar\\.gz\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_watch(tmp_path)
mock_run.assert_called_once()
# Verify watch was upgraded
content = watch_path.read_text()
assert "Version: 5" in content
assert "Template: GitHub" in content
assert "Owner: owner" in content
assert "Project: repo" in content
def test_upgrades_pypi_watch(self, tmp_path):
"""Test upgrading a PyPI watch file."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
watch_path = debian_dir / "watch"
watch_path.write_text(
"version=4\n"
"https://pypi.debian.net/mypackage/mypackage-([\\d.]+)\\.tar\\.gz\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_watch(tmp_path)
mock_run.assert_called_once()
# Verify watch was upgraded
content = watch_path.read_text()
assert "Version: 5" in content
assert "Template: Pypi" in content
assert "Dist: mypackage" in content
def test_already_version_5(self, tmp_path):
"""Test when watch is already version 5."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
watch_path = debian_dir / "watch"
watch_path.write_text("Version: 5\n\nTemplate: GitHub\n")
with patch("debian_todo.run_command") as mock_run:
debian_todo.update_debian_watch(tmp_path)
mock_run.assert_not_called()
def test_missing_watch_file(self, tmp_path):
"""Test when debian/watch doesn't exist."""
debian_todo.update_debian_watch(tmp_path) # Should not raise
class TestAddSalsaCi:
"""Tests for add_salsa_ci function."""
def test_adds_salsa_ci_yml(self, tmp_path):
"""Test adding debian/salsa-ci.yml."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
control_path = debian_dir / "control"
control_path.write_text(
"Source: mypkg\n"
"Vcs-Git: https://salsa.debian.org/python-team/packages/mypkg.git\n"
)
with patch("debian_todo.run_command") as mock_run:
debian_todo.add_salsa_ci(tmp_path)
assert mock_run.call_count == 2 # git add and dch
# Verify file was created
salsa_ci_path = debian_dir / "salsa-ci.yml"
assert salsa_ci_path.exists()
content = salsa_ci_path.read_text()
assert "salsa-ci-team/pipeline" in content
def test_already_exists(self, tmp_path):
"""Test when debian/salsa-ci.yml already exists."""
debian_dir = tmp_path / "debian"
debian_dir.mkdir()
salsa_ci_path = debian_dir / "salsa-ci.yml"
salsa_ci_path.write_text("existing content\n")
with patch("debian_todo.run_command") as mock_run:
debian_todo.add_salsa_ci(tmp_path)
mock_run.assert_not_called()
def test_missing_control_file(self, tmp_path):
"""Test when debian/control doesn't exist."""
debian_todo.add_salsa_ci(tmp_path) # Should not raise
class TestAddChangelogEntry:
"""Tests for add_changelog_entry function."""
def test_adds_changelog_entry(self, tmp_path):
with patch.object(
debian_todo, "extract_upstream_version_from_git_log"
) as mock_extract:
mock_extract.return_value = "1.2.3"
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
result = debian_todo.add_changelog_entry(tmp_path)
assert result is True
mock_run.assert_called_once_with(
[
"dch",
"--release-heuristic",
"log",
"--newversion",
"1.2.3-1",
"New upstream release.",
],
cwd=tmp_path,
)
def test_dch_fails(self, tmp_path):
with patch.object(
debian_todo, "extract_upstream_version_from_git_log"
) as mock_extract:
mock_extract.return_value = "1.2.3"
with patch("debian_todo.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=1)
result = debian_todo.add_changelog_entry(tmp_path)
assert result is False
def test_no_version_found(self, tmp_path):
with patch.object(
debian_todo, "extract_upstream_version_from_git_log"
) as mock_extract:
mock_extract.return_value = None
result = debian_todo.add_changelog_entry(tmp_path)
assert result is False
class TestUpdatePackage:
"""Tests for update_package function."""
def test_package_not_found(self, monkeypatch):
monkeypatch.setattr(
debian_todo, "load_source_info_map", lambda: {}
)
with pytest.raises(debian_todo.PackageNotFoundError) as exc_info:
debian_todo.update_package("nonexistent-pkg")
assert "nonexistent-pkg" in str(exc_info.value)
def test_package_no_vcs_git(self, monkeypatch):
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "", "uploaders": ""}},
)
with pytest.raises(debian_todo.PackageNotFoundError) as exc_info:
debian_todo.update_package("mypkg")
assert "mypkg" in str(exc_info.value)
def test_new_checkout(self, tmp_path, monkeypatch):
"""Test checking out a package that doesn't exist yet."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
subprocess_calls = []
def mock_subprocess_run(cmd, **kwargs):
subprocess_calls.append((cmd, kwargs))
result = MagicMock(returncode=0, stdout="", stderr="")
# After salsa checkout, create the repo directory
if cmd[0] == "salsa":
pkg_dir = tmp_path / "python" / "mypkg"
repo_dir = pkg_dir / "mypkg"
repo_dir.mkdir(parents=True, exist_ok=True)
return result
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(
debian_todo,
"extract_upstream_version_from_git_log",
lambda _: "1.0.0",
)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
# Mock the update functions to avoid needing debian/ directory
with patch("debian_todo.run_package_updates"):
debian_todo.update_package("mypkg")
# Check salsa checkout was called
assert subprocess_calls[0][0] == ["salsa", "checkout", "python-team/deps/mypkg"]
# Check gbp pq commands were called
assert subprocess_calls[1][0] == ["gbp", "pq", "import"]
assert subprocess_calls[2][0] == ["gbp", "pq", "switch"]
# Check gbp import-orig was called
assert subprocess_calls[3][0] == [
"gbp", "import-orig", "--uscan", "--pristine-tar", "--no-interactive"
]
# Check dch was called
assert subprocess_calls[4][0][0] == "dch"
def test_existing_checkout_clean(self, tmp_path, monkeypatch):
"""Test updating a package that's already checked out and clean."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "homeassistant-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
# Create existing repo directory
repo_dir = tmp_path / "ha" / "mypkg" / "mypkg"
repo_dir.mkdir(parents=True)
subprocess_calls = []
def mock_subprocess_run(cmd, **kwargs):
subprocess_calls.append((cmd, kwargs))
if cmd == ["git", "status", "--porcelain"]:
return MagicMock(stdout="", returncode=0)
return MagicMock(returncode=0, stdout="", stderr="")
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(
debian_todo,
"extract_upstream_version_from_git_log",
lambda _: "2.0.0",
)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
# Mock the update functions to avoid needing debian/ directory
with patch("debian_todo.run_package_updates"):
debian_todo.update_package("mypkg")
# Should not call salsa checkout
salsa_calls = [c for c in subprocess_calls if c[0][0] == "salsa"]
assert len(salsa_calls) == 0
# Should call git status, gbp pq, gbp import-orig, dch
assert subprocess_calls[0][0] == ["git", "status", "--porcelain"]
assert subprocess_calls[1][0] == ["gbp", "pq", "import"]
assert subprocess_calls[2][0] == ["gbp", "pq", "switch"]
assert subprocess_calls[3][0][0] == "gbp"
assert subprocess_calls[4][0][0] == "dch"
def test_existing_checkout_dirty(self, tmp_path, monkeypatch):
"""Test that update aborts if there are uncommitted changes."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
# Create existing repo directory
repo_dir = tmp_path / "python" / "mypkg" / "mypkg"
repo_dir.mkdir(parents=True)
def mock_subprocess_run(cmd, **kwargs):
if cmd == ["git", "status", "--porcelain"]:
return MagicMock(stdout=" M dirty.txt\n", returncode=0)
return MagicMock(returncode=0)
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
with pytest.raises(debian_todo.RepositoryStateError) as exc_info:
debian_todo.update_package("mypkg")
assert "uncommitted changes" in str(exc_info.value)
def test_salsa_checkout_fails(self, tmp_path, monkeypatch):
"""Test error handling when salsa checkout fails."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
def mock_subprocess_run(cmd, **kwargs):
if cmd[0] == "salsa":
return MagicMock(returncode=1, stdout="", stderr="")
return MagicMock(returncode=0, stdout="", stderr="")
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
with pytest.raises(debian_todo.ExternalCommandError) as exc_info:
debian_todo.update_package("mypkg")
assert "salsa" in str(exc_info.value)
def test_gbp_import_fails(self, tmp_path, monkeypatch):
"""Test error handling when gbp import-orig fails."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
# Create existing repo directory
repo_dir = tmp_path / "python" / "mypkg" / "mypkg"
repo_dir.mkdir(parents=True)
call_count = {"count": 0}
def mock_subprocess_run(cmd, **kwargs):
if cmd == ["git", "status", "--porcelain"]:
return MagicMock(stdout="", returncode=0)
if cmd[0] == "gbp":
call_count["count"] += 1
# Fail on third gbp call (import-orig), succeed on pq commands
if call_count["count"] >= 3:
return MagicMock(returncode=1, stdout="", stderr="")
return MagicMock(returncode=0, stdout="", stderr="")
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
with pytest.raises(debian_todo.ExternalCommandError) as exc_info:
debian_todo.update_package("mypkg")
assert "gbp" in str(exc_info.value)
def test_changelog_entry_fails(self, tmp_path, monkeypatch):
"""Test error handling when adding changelog entry fails."""
monkeypatch.setattr(
debian_todo,
"load_source_info_map",
lambda: {"mypkg": {"vcs_git": "python-team", "uploaders": ""}},
)
monkeypatch.setattr(debian_todo, "DEBIAN_SRC_BASE", tmp_path)
# Create existing repo directory
repo_dir = tmp_path / "python" / "mypkg" / "mypkg"
repo_dir.mkdir(parents=True)
def mock_subprocess_run(cmd, **kwargs):
if cmd == ["git", "status", "--porcelain"]:
return MagicMock(stdout="", returncode=0)
return MagicMock(returncode=0, stdout="", stderr="")
monkeypatch.setattr(debian_todo.subprocess, "run", mock_subprocess_run)
monkeypatch.setattr(
debian_todo,
"extract_upstream_version_from_git_log",
lambda _: None,
)
monkeypatch.setattr(debian_todo.shutil, "which", lambda _: "/usr/bin/tool")
with pytest.raises(debian_todo.ExternalCommandError) as exc_info:
debian_todo.update_package("mypkg")
assert "upstream version" in str(exc_info.value)