Release version 0.4.3
This commit is contained in:
@@ -11,8 +11,8 @@ class TestIntegrationBranchCommands(unittest.TestCase):
|
||||
Integration tests for the `pkgmgr branch` CLI wiring.
|
||||
|
||||
These tests execute the real entry point (main.py) and mock
|
||||
the high-level `open_branch` helper to ensure that argument
|
||||
parsing and dispatch behave as expected.
|
||||
the high-level helpers to ensure that argument parsing and
|
||||
dispatch behave as expected.
|
||||
"""
|
||||
|
||||
def _run_pkgmgr(self, extra_args: list[str]) -> None:
|
||||
@@ -64,6 +64,46 @@ class TestIntegrationBranchCommands(unittest.TestCase):
|
||||
self.assertEqual(kwargs.get("base_branch"), "main")
|
||||
self.assertEqual(kwargs.get("cwd"), ".")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# close subcommand
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.branch.close_branch")
|
||||
def test_branch_close_with_name_and_base(self, mock_close_branch) -> None:
|
||||
"""
|
||||
`pkgmgr branch close feature/test --base develop` must forward
|
||||
the name and base branch to close_branch() with cwd=".".
|
||||
"""
|
||||
self._run_pkgmgr(
|
||||
["branch", "close", "feature/test", "--base", "develop"]
|
||||
)
|
||||
|
||||
mock_close_branch.assert_called_once()
|
||||
_, kwargs = mock_close_branch.call_args
|
||||
self.assertEqual(kwargs.get("name"), "feature/test")
|
||||
self.assertEqual(kwargs.get("base_branch"), "develop")
|
||||
self.assertEqual(kwargs.get("cwd"), ".")
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.branch.close_branch")
|
||||
def test_branch_close_without_name_uses_default_base(
|
||||
self,
|
||||
mock_close_branch,
|
||||
) -> None:
|
||||
"""
|
||||
`pkgmgr branch close` without a name must still call close_branch(),
|
||||
passing name=None and the default base branch 'main'.
|
||||
|
||||
The branch helper will then resolve the actual base (main/master)
|
||||
internally.
|
||||
"""
|
||||
self._run_pkgmgr(["branch", "close"])
|
||||
|
||||
mock_close_branch.assert_called_once()
|
||||
_, kwargs = mock_close_branch.call_args
|
||||
self.assertIsNone(kwargs.get("name"))
|
||||
self.assertEqual(kwargs.get("base_branch"), "main")
|
||||
self.assertEqual(kwargs.get("cwd"), ".")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,99 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
End-to-end style integration tests for the `pkgmgr release` CLI command.
|
||||
|
||||
These tests exercise the top-level `pkgmgr` entry point by invoking
|
||||
the module as `__main__` and verifying that the underlying
|
||||
`pkgmgr.release.release()` function is called with the expected
|
||||
arguments, in particular the new `close` flag.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
)
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestIntegrationReleaseCommand(unittest.TestCase):
|
||||
def _run_pkgmgr(
|
||||
self,
|
||||
argv: list[str],
|
||||
expect_success: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Run the main entry point with the given argv and assert on success/failure.
|
||||
"""Integration tests for `pkgmgr release` wiring."""
|
||||
|
||||
argv must include the program name as argv[0], e.g. "":
|
||||
["", "release", "patch", "pkgmgr", "--preview"]
|
||||
def _run_pkgmgr(self, argv: list[str]) -> None:
|
||||
"""
|
||||
Helper to invoke the `pkgmgr` console script via `run_module`.
|
||||
|
||||
This simulates a real CLI call like:
|
||||
|
||||
pkgmgr release minor --preview --close
|
||||
"""
|
||||
cmd_repr = " ".join(argv[1:])
|
||||
original_argv = list(sys.argv)
|
||||
|
||||
try:
|
||||
sys.argv = argv
|
||||
try:
|
||||
# Execute main.py as if called via `python main.py ...`
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else 1
|
||||
if expect_success and code != 0:
|
||||
print()
|
||||
print(f"[TEST] Command : {cmd_repr}")
|
||||
print(f"[TEST] Exit code : {code}")
|
||||
raise AssertionError(
|
||||
f"{cmd_repr!r} failed with exit code {code}. "
|
||||
"Scroll up to inspect the output printed before failure."
|
||||
) from exc
|
||||
if not expect_success and code == 0:
|
||||
print()
|
||||
print(f"[TEST] Command : {cmd_repr}")
|
||||
print(f"[TEST] Exit code : {code}")
|
||||
raise AssertionError(
|
||||
f"{cmd_repr!r} unexpectedly succeeded with exit code 0."
|
||||
) from exc
|
||||
else:
|
||||
# No SystemExit: treat as success when expect_success is True,
|
||||
# otherwise as a failure (we expected a non-zero exit).
|
||||
if not expect_success:
|
||||
raise AssertionError(
|
||||
f"{cmd_repr!r} returned normally (expected non-zero exit)."
|
||||
)
|
||||
# Entry point: the `pkgmgr` module is the console script.
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
def test_release_for_unknown_repo_fails_cleanly(self) -> None:
|
||||
@patch("pkgmgr.release.release")
|
||||
def test_release_without_close_flag(self, mock_release) -> None:
|
||||
"""
|
||||
Releasing a non-existent repository identifier must fail
|
||||
with a non-zero exit code, but without crashing the interpreter.
|
||||
Calling `pkgmgr release patch --preview` should *not* enable
|
||||
the `close` flag by default.
|
||||
"""
|
||||
argv = [
|
||||
"",
|
||||
"release",
|
||||
"patch",
|
||||
"does-not-exist-xyz",
|
||||
]
|
||||
self._run_pkgmgr(argv, expect_success=False)
|
||||
self._run_pkgmgr(["pkgmgr", "release", "patch", "--preview"])
|
||||
|
||||
def test_release_preview_for_pkgmgr_repository(self) -> None:
|
||||
"""
|
||||
Sanity-check the happy path for the CLI:
|
||||
mock_release.assert_called_once()
|
||||
_args, kwargs = mock_release.call_args
|
||||
|
||||
- Runs `pkgmgr release patch pkgmgr --preview`
|
||||
- Must exit with code 0
|
||||
- Uses the real configuration + repository selection
|
||||
- Exercises the new --preview mode end-to-end.
|
||||
"""
|
||||
argv = [
|
||||
"",
|
||||
"release",
|
||||
"patch",
|
||||
"pkgmgr",
|
||||
"--preview",
|
||||
]
|
||||
# CLI wiring
|
||||
self.assertEqual(kwargs.get("release_type"), "patch")
|
||||
self.assertTrue(kwargs.get("preview"), "preview should be True when --preview is used")
|
||||
# Default: no --close → close=False
|
||||
self.assertFalse(kwargs.get("close"), "close must be False when --close is not given")
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(PROJECT_ROOT)
|
||||
self._run_pkgmgr(argv, expect_success=True)
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
@patch("pkgmgr.release.release")
|
||||
def test_release_with_close_flag(self, mock_release) -> None:
|
||||
"""
|
||||
Calling `pkgmgr release minor --preview --close` should pass
|
||||
close=True into pkgmgr.release.release().
|
||||
"""
|
||||
self._run_pkgmgr(["pkgmgr", "release", "minor", "--preview", "--close"])
|
||||
|
||||
mock_release.assert_called_once()
|
||||
_args, kwargs = mock_release.call_args
|
||||
|
||||
# CLI wiring
|
||||
self.assertEqual(kwargs.get("release_type"), "minor")
|
||||
self.assertTrue(kwargs.get("preview"), "preview should be True when --preview is used")
|
||||
# With --close → close=True
|
||||
self.assertTrue(kwargs.get("close"), "close must be True when --close is given")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
0
tests/unit/pkgmgr/cli_core/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/commands/__init__.py
Normal file
0
tests/unit/pkgmgr/cli_core/commands/__init__.py
Normal file
206
tests/unit/pkgmgr/cli_core/commands/test_release.py
Normal file
206
tests/unit/pkgmgr/cli_core/commands/test_release.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Unit tests for pkgmgr.cli_core.commands.release.
|
||||
|
||||
These tests focus on the wiring layer:
|
||||
- Argument handling for the release command as defined by the
|
||||
top-level parser (cli_core.parser.create_parser).
|
||||
- Correct invocation of pkgmgr.release.release(...) for the
|
||||
selected repositories.
|
||||
- Behaviour of --preview, --list, --close, and -f/--force.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import List
|
||||
from unittest.mock import patch, call
|
||||
|
||||
import argparse
|
||||
import unittest
|
||||
|
||||
|
||||
class TestReleaseCommand(unittest.TestCase):
|
||||
"""
|
||||
Tests for the `pkgmgr release` CLI wiring.
|
||||
"""
|
||||
|
||||
def _make_ctx(self, all_repos: List[dict]) -> SimpleNamespace:
|
||||
"""
|
||||
Create a minimal CLIContext-like object for tests.
|
||||
|
||||
Only the attributes that handle_release() uses are provided.
|
||||
"""
|
||||
return SimpleNamespace(
|
||||
config_merged={},
|
||||
repositories_base_dir="/base/dir",
|
||||
all_repositories=all_repos,
|
||||
binaries_dir="/bin",
|
||||
user_config_path="/tmp/config.yaml",
|
||||
)
|
||||
|
||||
def _parse_release_args(self, argv: List[str]) -> argparse.Namespace:
|
||||
"""
|
||||
Build a real top-level parser and parse the given argv list
|
||||
to obtain the Namespace for the `release` command.
|
||||
"""
|
||||
from pkgmgr.cli_core.parser import create_parser
|
||||
|
||||
parser = create_parser("test parser")
|
||||
args = parser.parse_args(argv)
|
||||
self.assertEqual(args.command, "release")
|
||||
return args
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.release.os.path.isdir", return_value=True)
|
||||
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||
@patch("pkgmgr.cli_core.commands.release.os.chdir")
|
||||
@patch("pkgmgr.cli_core.commands.release.os.getcwd", return_value="/cwd")
|
||||
def test_release_with_close_and_message(
|
||||
self,
|
||||
mock_getcwd,
|
||||
mock_chdir,
|
||||
mock_get_repo_identifier,
|
||||
mock_get_repo_dir,
|
||||
mock_run_release,
|
||||
mock_isdir,
|
||||
) -> None:
|
||||
"""
|
||||
The release handler should call pkgmgr.release.release() with:
|
||||
- release_type (e.g. minor)
|
||||
- provided message
|
||||
- preview flag
|
||||
- force flag
|
||||
- close flag
|
||||
|
||||
It must change into the repository directory and then back.
|
||||
"""
|
||||
from pkgmgr.cli_core.commands.release import handle_release
|
||||
|
||||
repo = {"name": "dummy-repo"}
|
||||
selected = [repo]
|
||||
ctx = self._make_ctx(selected)
|
||||
|
||||
mock_get_repo_identifier.return_value = "dummy-id"
|
||||
mock_get_repo_dir.return_value = "/repos/dummy"
|
||||
|
||||
argv = [
|
||||
"release",
|
||||
"minor",
|
||||
"dummy-id",
|
||||
"-m",
|
||||
"Close branch after minor release",
|
||||
"--close",
|
||||
"-f",
|
||||
]
|
||||
args = self._parse_release_args(argv)
|
||||
|
||||
handle_release(args, ctx, selected)
|
||||
|
||||
# We should have changed into the repo dir and then back.
|
||||
mock_chdir.assert_has_calls(
|
||||
[call("/repos/dummy"), call("/cwd")]
|
||||
)
|
||||
|
||||
# And run_release should be invoked once with the expected parameters.
|
||||
mock_run_release.assert_called_once_with(
|
||||
pyproject_path="pyproject.toml",
|
||||
changelog_path="CHANGELOG.md",
|
||||
release_type="minor",
|
||||
message="Close branch after minor release",
|
||||
preview=False,
|
||||
force=True,
|
||||
close=True,
|
||||
)
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.release.os.path.isdir", return_value=True)
|
||||
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||
@patch("pkgmgr.cli_core.commands.release.os.chdir")
|
||||
@patch("pkgmgr.cli_core.commands.release.os.getcwd", return_value="/cwd")
|
||||
def test_release_preview_mode(
|
||||
self,
|
||||
mock_getcwd,
|
||||
mock_chdir,
|
||||
mock_get_repo_identifier,
|
||||
mock_get_repo_dir,
|
||||
mock_run_release,
|
||||
mock_isdir,
|
||||
) -> None:
|
||||
"""
|
||||
In preview mode, the handler should pass preview=True to the
|
||||
release helper and force=False by default.
|
||||
"""
|
||||
from pkgmgr.cli_core.commands.release import handle_release
|
||||
|
||||
repo = {"name": "dummy-repo"}
|
||||
selected = [repo]
|
||||
ctx = self._make_ctx(selected)
|
||||
|
||||
mock_get_repo_identifier.return_value = "dummy-id"
|
||||
mock_get_repo_dir.return_value = "/repos/dummy"
|
||||
|
||||
argv = [
|
||||
"release",
|
||||
"patch",
|
||||
"dummy-id",
|
||||
"--preview",
|
||||
]
|
||||
args = self._parse_release_args(argv)
|
||||
|
||||
handle_release(args, ctx, selected)
|
||||
|
||||
mock_run_release.assert_called_once_with(
|
||||
pyproject_path="pyproject.toml",
|
||||
changelog_path="CHANGELOG.md",
|
||||
release_type="patch",
|
||||
message=None,
|
||||
preview=True,
|
||||
force=False,
|
||||
close=False,
|
||||
)
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.release.run_release")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_dir")
|
||||
@patch("pkgmgr.cli_core.commands.release.get_repo_identifier")
|
||||
def test_release_list_mode_does_not_invoke_helper(
|
||||
self,
|
||||
mock_get_repo_identifier,
|
||||
mock_get_repo_dir,
|
||||
mock_run_release,
|
||||
) -> None:
|
||||
"""
|
||||
When --list is provided, the handler should print the list of affected
|
||||
repositories and must NOT invoke run_release().
|
||||
"""
|
||||
from pkgmgr.cli_core.commands.release import handle_release
|
||||
|
||||
repo1 = {"name": "repo-1"}
|
||||
repo2 = {"name": "repo-2"}
|
||||
selected = [repo1, repo2]
|
||||
ctx = self._make_ctx(selected)
|
||||
|
||||
mock_get_repo_identifier.side_effect = ["id-1", "id-2"]
|
||||
|
||||
argv = [
|
||||
"release",
|
||||
"major",
|
||||
"--list",
|
||||
]
|
||||
args = self._parse_release_args(argv)
|
||||
|
||||
handle_release(args, ctx, selected)
|
||||
|
||||
mock_run_release.assert_not_called()
|
||||
self.assertEqual(
|
||||
mock_get_repo_identifier.call_args_list,
|
||||
[call(repo1, selected), call(repo2, selected)],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -66,6 +66,55 @@ class TestCliBranch(unittest.TestCase):
|
||||
self.assertEqual(call_kwargs.get("base_branch"), "main")
|
||||
self.assertEqual(call_kwargs.get("cwd"), ".")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# close subcommand
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.branch.close_branch")
|
||||
def test_handle_branch_close_forwards_args_to_close_branch(self, mock_close_branch) -> None:
|
||||
"""
|
||||
handle_branch('close') should call close_branch with name, base and cwd='.'.
|
||||
"""
|
||||
args = SimpleNamespace(
|
||||
command="branch",
|
||||
subcommand="close",
|
||||
name="feature/cli-close",
|
||||
base="develop",
|
||||
)
|
||||
|
||||
ctx = self._dummy_ctx()
|
||||
|
||||
handle_branch(args, ctx)
|
||||
|
||||
mock_close_branch.assert_called_once()
|
||||
_, call_kwargs = mock_close_branch.call_args
|
||||
self.assertEqual(call_kwargs.get("name"), "feature/cli-close")
|
||||
self.assertEqual(call_kwargs.get("base_branch"), "develop")
|
||||
self.assertEqual(call_kwargs.get("cwd"), ".")
|
||||
|
||||
@patch("pkgmgr.cli_core.commands.branch.close_branch")
|
||||
def test_handle_branch_close_uses_default_base_when_not_set(self, mock_close_branch) -> None:
|
||||
"""
|
||||
If --base is not passed for 'close', argparse gives base='main'
|
||||
(default), and handle_branch should propagate that to close_branch.
|
||||
"""
|
||||
args = SimpleNamespace(
|
||||
command="branch",
|
||||
subcommand="close",
|
||||
name=None,
|
||||
base="main",
|
||||
)
|
||||
|
||||
ctx = self._dummy_ctx()
|
||||
|
||||
handle_branch(args, ctx)
|
||||
|
||||
mock_close_branch.assert_called_once()
|
||||
_, call_kwargs = mock_close_branch.call_args
|
||||
self.assertIsNone(call_kwargs.get("name"))
|
||||
self.assertEqual(call_kwargs.get("base_branch"), "main")
|
||||
self.assertEqual(call_kwargs.get("cwd"), ".")
|
||||
|
||||
def test_handle_branch_unknown_subcommand_exits_with_code_2(self) -> None:
|
||||
"""
|
||||
Unknown branch subcommand should result in SystemExit(2).
|
||||
|
||||
@@ -365,6 +365,7 @@ class TestUpdateDebianChangelog(unittest.TestCase):
|
||||
|
||||
|
||||
class TestReleaseOrchestration(unittest.TestCase):
|
||||
@patch("pkgmgr.release.sys.stdin.isatty", return_value=False)
|
||||
@patch("pkgmgr.release._run_git_command")
|
||||
@patch("pkgmgr.release.update_debian_changelog")
|
||||
@patch("pkgmgr.release.update_spec_version")
|
||||
@@ -387,6 +388,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
||||
mock_update_spec,
|
||||
mock_update_debian_changelog,
|
||||
mock_run_git_command,
|
||||
mock_isatty,
|
||||
) -> None:
|
||||
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
||||
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
||||
@@ -449,6 +451,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
||||
self.assertIn("git push origin develop", git_calls)
|
||||
self.assertIn("git push origin --tags", git_calls)
|
||||
|
||||
@patch("pkgmgr.release.sys.stdin.isatty", return_value=False)
|
||||
@patch("pkgmgr.release._run_git_command")
|
||||
@patch("pkgmgr.release.update_debian_changelog")
|
||||
@patch("pkgmgr.release.update_spec_version")
|
||||
@@ -471,6 +474,7 @@ class TestReleaseOrchestration(unittest.TestCase):
|
||||
mock_update_spec,
|
||||
mock_update_debian_changelog,
|
||||
mock_run_git_command,
|
||||
mock_isatty,
|
||||
) -> None:
|
||||
mock_determine_current_version.return_value = SemVer(1, 2, 3)
|
||||
mock_bump_semver.return_value = SemVer(1, 2, 4)
|
||||
|
||||
Reference in New Issue
Block a user