"""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) 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 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 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 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 \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 \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 \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)