Files
pkgmgr/tests/e2e/test_changelog_commands.py
Kevin Veen-Birkenbach 7764a2946c fix: catch concrete exceptions instead of bare Exception
Replace 46 blind `except Exception` handlers with the exception types the
guarded code can actually raise, so an unforeseen failure surfaces instead
of being silently swallowed. The types were derived per site from the try
block: file reads get (OSError, UnicodeDecodeError), TOML and YAML parsing
add their decode errors, int() guards get ValueError, imports get
ImportError, subprocess calls get (OSError, subprocess.SubprocessError),
git queries get GitRunError, and importlib.metadata lookups get
PackageNotFoundError.

Three handlers stay blind, now with a stated reason: the per-repository
loops in install and update are batch boundaries where one failing
repository must never abort the run.

Three imports come along because the narrowing needs them: GitRunError in
the changelog and version commands, and http.client in token validation --
urllib raises HTTPException, which is not an OSError subclass and would
otherwise have escaped.

Add tests/integration/test_error_path_degradation.py, which drives 44 of
the 46 handlers into their except branch and asserts the documented
degradation. Faults are injected for real rather than mocked wherever
possible: undecodable bytes, malformed YAML, digit strings past
sys.get_int_max_str_digits(), a real HTTPError with an unreadable body.
That approach paid for itself immediately -- tomllib.load() decodes
internally and raises UnicodeDecodeError, which was missing from the
pyproject handlers and would have turned a malformed pyproject.toml into
a traceback.

The two handlers left uncovered are the branch-close path in _release_impl,
reachable only through a full release, and the jinja2 import guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:37:14 +02:00

126 lines
4.2 KiB
Python

from __future__ import annotations
import os
import runpy
import sys
import unittest
from test_version_commands import (
PROJECT_ROOT,
_load_pkgmgr_repo_dir,
)
class TestIntegrationChangelogCommands(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
"""
Versuche, das pkgmgr-Repository-Verzeichnis aus der Config zu laden.
Wenn es im aktuellen Test-Container nicht existiert, merken wir uns
None und überspringen repo-spezifische Tests später sauber.
"""
try:
repo_dir = _load_pkgmgr_repo_dir()
except (OSError, RuntimeError, ValueError):
repo_dir = None
if repo_dir is not None and not os.path.isdir(repo_dir):
repo_dir = None
cls.pkgmgr_repo_dir = repo_dir
def _run_pkgmgr_changelog(
self,
extra_args: list[str] | None = None,
cwd: str | None = None,
) -> None:
"""
Helper that executes the pkgmgr CLI with the 'changelog' command
via runpy, similar to the existing version integration tests.
"""
if extra_args is None:
extra_args = []
cmd_repr = "pkgmgr changelog " + " ".join(extra_args)
original_argv = list(sys.argv)
original_cwd = os.getcwd()
try:
if cwd is not None and os.path.isdir(cwd):
os.chdir(cwd)
# Simulate CLI invocation: pkgmgr changelog <args...>
sys.argv = ["pkgmgr", "changelog"] + list(extra_args)
try:
runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0:
print()
print(f"[TEST] Command : {cmd_repr}")
print(f"[TEST] Working directory: {os.getcwd()}")
print(f"[TEST] Exit code : {code}")
raise AssertionError(
f"{cmd_repr!r} failed with exit code {code}. "
"Scroll up to inspect the pkgmgr output before failure."
) from exc
finally:
os.chdir(original_cwd)
sys.argv = original_argv
def test_changelog_default_range_current_repo(self) -> None:
"""
Run 'pkgmgr changelog' inside the pkgmgr repo, using the default range
(last two SemVer tags or fallback to full history).
Wird übersprungen, wenn das pkgmgr-Repo in dieser Umgebung
nicht lokal vorhanden ist.
"""
if self.pkgmgr_repo_dir is None:
self.skipTest(
"pkgmgr repo directory not available in this environment; "
"skipping repo-local changelog test."
)
self._run_pkgmgr_changelog(extra_args=[], cwd=self.pkgmgr_repo_dir)
def test_changelog_explicit_range_head_history(self) -> None:
"""
Run 'pkgmgr changelog HEAD~5..HEAD' inside the pkgmgr repo.
Selbst wenn HEAD~5 nicht existiert, sollte der Befehl den
GitBaseError intern behandeln und mit Exit-Code 0 beenden
(es wird dann eine [ERROR]-Zeile gedruckt).
Wird übersprungen, wenn das pkgmgr-Repo nicht lokal vorhanden ist.
"""
if self.pkgmgr_repo_dir is None:
self.skipTest(
"pkgmgr repo directory not available in this environment; "
"skipping repo-local changelog range test."
)
self._run_pkgmgr_changelog(
extra_args=["HEAD~5..HEAD"],
cwd=self.pkgmgr_repo_dir,
)
def test_changelog_all_repositories_default(self) -> None:
"""
Run 'pkgmgr changelog --all' from the project root to ensure
that repository selection + changelog pipeline work in the
multi-repo scenario.
Dieser Test ist robust, selbst wenn einige Repos aus der Config
physisch nicht existieren: handle_changelog überspringt sie
mit einer INFO-Meldung.
"""
self._run_pkgmgr_changelog(
extra_args=["--all"],
cwd=PROJECT_ROOT,
)
if __name__ == "__main__":
unittest.main()