Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b998741f | ||
|
|
172c734866 | ||
|
|
1b483e178d | ||
|
|
78693225f1 | ||
|
|
ca08c84789 | ||
|
|
e930b422e5 | ||
|
|
0833d04376 | ||
|
|
55f36d76ec |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,3 +1,18 @@
|
||||
## [0.7.11] - 2025-12-09
|
||||
|
||||
* test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
|
||||
## [0.7.10] - 2025-12-09
|
||||
|
||||
* Fixed test_install_pkgmgr_shallow.py
|
||||
|
||||
|
||||
## [0.7.9] - 2025-12-09
|
||||
|
||||
* 'main' and 'master' are now both accepted as branches for branch close merge
|
||||
|
||||
|
||||
## [0.7.8] - 2025-12-09
|
||||
|
||||
* Missing pyproject.toml doesn't lead to an error during release
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=0.7.8
|
||||
pkgver=0.7.11
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
18
debian/changelog
vendored
18
debian/changelog
vendored
@@ -1,3 +1,21 @@
|
||||
package-manager (0.7.11-1) unstable; urgency=medium
|
||||
|
||||
* test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 23:16:46 +0100
|
||||
|
||||
package-manager (0.7.10-1) unstable; urgency=medium
|
||||
|
||||
* Fixed test_install_pkgmgr_shallow.py
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 22:57:08 +0100
|
||||
|
||||
package-manager (0.7.9-1) unstable; urgency=medium
|
||||
|
||||
* 'main' and 'master' are now both accepted as branches for branch close merge
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 21:19:13 +0100
|
||||
|
||||
package-manager (0.7.8-1) unstable; urgency=medium
|
||||
|
||||
* Missing pyproject.toml doesn't lead to an error during release
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "0.7.8";
|
||||
version = "0.7.11";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 0.7.8
|
||||
Version: 0.7.11
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -77,6 +77,15 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
||||
/usr/lib/package-manager/
|
||||
|
||||
%changelog
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.11-1
|
||||
- test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.10-1
|
||||
- Fixed test_install_pkgmgr_shallow.py
|
||||
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.9-1
|
||||
- 'main' and 'master' are now both accepted as branches for branch close merge
|
||||
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.8-1
|
||||
- Missing pyproject.toml doesn't lead to an error during release
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pkgmgr/branch_commands.py
|
||||
# pkgmgr/actions/branch/__init__.py
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -16,30 +16,43 @@ from typing import Optional
|
||||
from pkgmgr.core.git import run_git, GitError, get_current_branch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Branch creation (open)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def open_branch(
|
||||
name: Optional[str],
|
||||
base_branch: str = "main",
|
||||
fallback_base: str = "master",
|
||||
cwd: str = ".",
|
||||
) -> None:
|
||||
"""
|
||||
Create and push a new feature branch on top of `base_branch`.
|
||||
Create and push a new feature branch on top of a base branch.
|
||||
|
||||
The base branch is resolved by:
|
||||
1. Trying 'base_branch' (default: 'main')
|
||||
2. Falling back to 'fallback_base' (default: 'master')
|
||||
|
||||
Steps:
|
||||
1) git fetch origin
|
||||
2) git checkout <base_branch>
|
||||
3) git pull origin <base_branch>
|
||||
4) git checkout -b <name>
|
||||
5) git push -u origin <name>
|
||||
1) git fetch origin
|
||||
2) git checkout <resolved_base>
|
||||
3) git pull origin <resolved_base>
|
||||
4) git checkout -b <name>
|
||||
5) git push -u origin <name>
|
||||
|
||||
If `name` is None or empty, the user is prompted on stdin.
|
||||
If `name` is None or empty, the user is prompted to enter one.
|
||||
"""
|
||||
|
||||
# Request name interactively if not provided
|
||||
if not name:
|
||||
name = input("Enter new branch name: ").strip()
|
||||
|
||||
if not name:
|
||||
raise RuntimeError("Branch name must not be empty.")
|
||||
|
||||
# Resolve which base branch to use (main or master)
|
||||
resolved_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||
|
||||
# 1) Fetch from origin
|
||||
try:
|
||||
run_git(["fetch", "origin"], cwd=cwd)
|
||||
@@ -50,18 +63,18 @@ def open_branch(
|
||||
|
||||
# 2) Checkout base branch
|
||||
try:
|
||||
run_git(["checkout", base_branch], cwd=cwd)
|
||||
run_git(["checkout", resolved_base], cwd=cwd)
|
||||
except GitError as exc:
|
||||
raise RuntimeError(
|
||||
f"Failed to checkout base branch {base_branch!r}: {exc}"
|
||||
f"Failed to checkout base branch {resolved_base!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 3) Pull latest changes on base
|
||||
# 3) Pull latest changes for base branch
|
||||
try:
|
||||
run_git(["pull", "origin", base_branch], cwd=cwd)
|
||||
run_git(["pull", "origin", resolved_base], cwd=cwd)
|
||||
except GitError as exc:
|
||||
raise RuntimeError(
|
||||
f"Failed to pull latest changes for base branch {base_branch!r}: {exc}"
|
||||
f"Failed to pull latest changes for base branch {resolved_base!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 4) Create new branch
|
||||
@@ -69,10 +82,10 @@ def open_branch(
|
||||
run_git(["checkout", "-b", name], cwd=cwd)
|
||||
except GitError as exc:
|
||||
raise RuntimeError(
|
||||
f"Failed to create new branch {name!r} from base {base_branch!r}: {exc}"
|
||||
f"Failed to create new branch {name!r} from base {resolved_base!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 5) Push and set upstream
|
||||
# 5) Push new branch to origin
|
||||
try:
|
||||
run_git(["push", "-u", "origin", name], cwd=cwd)
|
||||
except GitError as exc:
|
||||
@@ -81,15 +94,21 @@ def open_branch(
|
||||
) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base branch resolver (shared by open/close)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_base_branch(
|
||||
preferred: str,
|
||||
fallback: str,
|
||||
cwd: str,
|
||||
) -> str:
|
||||
"""
|
||||
Resolve the base branch to use for merging.
|
||||
Resolve the base branch to use.
|
||||
|
||||
Try `preferred` first (default: main),
|
||||
fall back to `fallback` (default: master).
|
||||
|
||||
Try `preferred` (default: main) first, then `fallback` (default: master).
|
||||
Raise RuntimeError if neither exists.
|
||||
"""
|
||||
for candidate in (preferred, fallback):
|
||||
@@ -104,6 +123,10 @@ def _resolve_base_branch(
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Branch closing (merge + deletion)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def close_branch(
|
||||
name: Optional[str],
|
||||
base_branch: str = "main",
|
||||
@@ -111,23 +134,22 @@ def close_branch(
|
||||
cwd: str = ".",
|
||||
) -> None:
|
||||
"""
|
||||
Merge a feature branch into the main/master branch and optionally delete it.
|
||||
Merge a feature branch into the base branch and delete it afterwards.
|
||||
|
||||
Steps:
|
||||
1) Determine branch name (argument or current branch)
|
||||
2) Resolve base branch (prefers `base_branch`, falls back to `fallback_base`)
|
||||
3) Ask for confirmation (y/N)
|
||||
4) git fetch origin
|
||||
5) git checkout <base>
|
||||
6) git pull origin <base>
|
||||
7) git merge --no-ff <name>
|
||||
8) git push origin <base>
|
||||
9) Delete branch locally and on origin
|
||||
|
||||
If the user does not confirm with 'y', the operation is aborted.
|
||||
1) Determine the branch name (argument or current branch)
|
||||
2) Resolve base branch (main/master)
|
||||
3) Ask for confirmation
|
||||
4) git fetch origin
|
||||
5) git checkout <base>
|
||||
6) git pull origin <base>
|
||||
7) git merge --no-ff <name>
|
||||
8) git push origin <base>
|
||||
9) Delete branch locally
|
||||
10) Delete branch on origin (best effort)
|
||||
"""
|
||||
|
||||
# 1) Determine which branch to close
|
||||
# 1) Determine which branch should be closed
|
||||
if not name:
|
||||
try:
|
||||
name = get_current_branch(cwd=cwd)
|
||||
@@ -137,7 +159,7 @@ def close_branch(
|
||||
if not name:
|
||||
raise RuntimeError("Branch name must not be empty.")
|
||||
|
||||
# 2) Resolve base branch (main/master)
|
||||
# 2) Resolve base branch
|
||||
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||
|
||||
if name == target_base:
|
||||
@@ -146,7 +168,7 @@ def close_branch(
|
||||
"Please specify a feature branch."
|
||||
)
|
||||
|
||||
# 3) Confirmation prompt
|
||||
# 3) Ask user for confirmation
|
||||
prompt = (
|
||||
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? "
|
||||
"(y/N): "
|
||||
@@ -164,7 +186,7 @@ def close_branch(
|
||||
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 5) Checkout base branch
|
||||
# 5) Checkout base
|
||||
try:
|
||||
run_git(["checkout", target_base], cwd=cwd)
|
||||
except GitError as exc:
|
||||
@@ -172,7 +194,7 @@ def close_branch(
|
||||
f"Failed to checkout base branch {target_base!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 6) Pull latest base
|
||||
# 6) Pull latest base state
|
||||
try:
|
||||
run_git(["pull", "origin", target_base], cwd=cwd)
|
||||
except GitError as exc:
|
||||
@@ -180,7 +202,7 @@ def close_branch(
|
||||
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 7) Merge feature branch into base
|
||||
# 7) Merge the feature branch
|
||||
try:
|
||||
run_git(["merge", "--no-ff", name], cwd=cwd)
|
||||
except GitError as exc:
|
||||
@@ -193,22 +215,21 @@ def close_branch(
|
||||
run_git(["push", "origin", target_base], cwd=cwd)
|
||||
except GitError as exc:
|
||||
raise RuntimeError(
|
||||
f"Failed to push base branch {target_base!r} to origin after merge: {exc}"
|
||||
f"Failed to push base branch {target_base!r} after merge: {exc}"
|
||||
) from exc
|
||||
|
||||
# 9) Delete feature branch locally
|
||||
# 9) Delete branch locally
|
||||
try:
|
||||
run_git(["branch", "-d", name], cwd=cwd)
|
||||
except GitError as exc:
|
||||
raise RuntimeError(
|
||||
f"Failed to delete local branch {name!r} after merge: {exc}"
|
||||
f"Failed to delete local branch {name!r}: {exc}"
|
||||
) from exc
|
||||
|
||||
# 10) Delete feature branch on origin (best effort)
|
||||
# 10) Delete branch on origin (best effort)
|
||||
try:
|
||||
run_git(["push", "origin", "--delete", name], cwd=cwd)
|
||||
except GitError as exc:
|
||||
# Remote delete is nice-to-have; surface as RuntimeError for clarity.
|
||||
raise RuntimeError(
|
||||
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
|
||||
) from exc
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "0.7.8"
|
||||
version = "0.7.11"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -13,6 +13,7 @@ dnf -y install \
|
||||
bash \
|
||||
curl-minimal \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
xz
|
||||
|
||||
dnf clean all
|
||||
|
||||
@@ -19,6 +19,7 @@ for distro in $DISTROS; do
|
||||
# Run the command and capture the output
|
||||
if OUTPUT=$(docker run --rm \
|
||||
-e PKGMGR_DEV=1 \
|
||||
-v pkgmgr_nix_store:/nix \
|
||||
-v "$(pwd):/src" \
|
||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||
"$IMAGE" 2>&1); then
|
||||
|
||||
@@ -8,14 +8,9 @@ for distro in $DISTROS; do
|
||||
echo ">>> Running E2E tests: $distro"
|
||||
echo "============================================================"
|
||||
|
||||
MOUNT_NIX=""
|
||||
if [[ "$distro" == "arch" ]]; then
|
||||
MOUNT_NIX="-v pkgmgr_nix_store:/nix"
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
$MOUNT_NIX \
|
||||
-v pkgmgr_nix_store:/nix \
|
||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||
-e PKGMGR_DEV=1 \
|
||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||
|
||||
@@ -7,6 +7,7 @@ echo "============================================================"
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-v pkgmgr_nix_store:/nix \
|
||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||
--workdir /src \
|
||||
-e PKGMGR_DEV=1 \
|
||||
|
||||
@@ -8,6 +8,7 @@ echo "============================================================"
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||
-v pkgmgr_nix_store:/nix \
|
||||
--workdir /src \
|
||||
-e PKGMGR_DEV=1 \
|
||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||
|
||||
@@ -35,8 +35,8 @@ def remove_pkgmgr_from_nix_profile() -> None:
|
||||
prints a descriptive format without an index column inside the container.
|
||||
|
||||
Instead, we directly try to remove possible names:
|
||||
- 'pkgmgr' (the actual name shown in `nix profile list`)
|
||||
- 'package-manager' (the name mentioned in Nix's own error hints)
|
||||
- 'pkgmgr'
|
||||
- 'package-manager'
|
||||
"""
|
||||
for spec in ("pkgmgr", "package-manager"):
|
||||
subprocess.run(
|
||||
@@ -45,18 +45,34 @@ def remove_pkgmgr_from_nix_profile() -> None:
|
||||
)
|
||||
|
||||
|
||||
def configure_git_safe_directory() -> None:
|
||||
"""
|
||||
Configure Git to treat /src as a safe directory.
|
||||
|
||||
Needed because /src is a bind-mounted repository in CI, often owned by a
|
||||
different UID. Modern Git aborts with:
|
||||
'fatal: detected dubious ownership in repository at /src/.git'
|
||||
|
||||
This fix applies ONLY inside this test container.
|
||||
"""
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "config", "--global", "--add", "safe.directory", "/src"],
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
print("[WARN] git not found – skipping safe.directory configuration")
|
||||
|
||||
|
||||
def pkgmgr_help_debug() -> None:
|
||||
"""
|
||||
Run `pkgmgr --help` after installation *inside an interactive bash shell*,
|
||||
print its output and return code, but never fail the test.
|
||||
|
||||
Reason:
|
||||
- The installer adds venv/alias setup into shell rc files (~/.bashrc, ~/.zshrc)
|
||||
- Those changes are only applied in a new interactive shell session.
|
||||
This ensures the installer’s shell RC changes are actually loaded.
|
||||
"""
|
||||
print("\n--- PKGMGR HELP (after installation, via bash -i) ---")
|
||||
|
||||
# Simulate a fresh interactive bash, so ~/.bashrc gets sourced
|
||||
proc = subprocess.run(
|
||||
["bash", "-i", "-c", "pkgmgr --help"],
|
||||
capture_output=True,
|
||||
@@ -76,10 +92,6 @@ def pkgmgr_help_debug() -> None:
|
||||
print(f"returncode: {proc.returncode}")
|
||||
print("--- END ---\n")
|
||||
|
||||
# Important: this is **debug-only**. Do NOT fail the test here.
|
||||
# If you ever want to hard-assert on this, you can add an explicit
|
||||
# assertion in the test method instead of here.
|
||||
|
||||
|
||||
class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||
def test_install_pkgmgr_self_install(self) -> None:
|
||||
@@ -87,12 +99,8 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||
End-to-end test that runs "python main.py install pkgmgr ..." inside
|
||||
the test container.
|
||||
|
||||
We isolate HOME into /tmp/pkgmgr-self-install so that:
|
||||
- ~/.config/pkgmgr points to an isolated test config area
|
||||
- ~/Repositories is owned by the current user inside the container
|
||||
(avoiding Nix's 'repository path is not owned by current user' error)
|
||||
HOME is isolated to avoid permission problems with Nix & repositories.
|
||||
"""
|
||||
# Use a dedicated HOME for this test to avoid permission/ownership issues
|
||||
temp_home = "/tmp/pkgmgr-self-install"
|
||||
os.makedirs(temp_home, exist_ok=True)
|
||||
|
||||
@@ -103,20 +111,24 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||
# Isolate HOME so that ~ expands to /tmp/pkgmgr-self-install
|
||||
os.environ["HOME"] = temp_home
|
||||
|
||||
# Optional: ensure XDG_* also use the temp HOME for extra isolation
|
||||
# Optional XDG override for a fully isolated environment
|
||||
os.environ.setdefault("XDG_CONFIG_HOME", os.path.join(temp_home, ".config"))
|
||||
os.environ.setdefault("XDG_CACHE_HOME", os.path.join(temp_home, ".cache"))
|
||||
os.environ.setdefault("XDG_DATA_HOME", os.path.join(temp_home, ".local", "share"))
|
||||
|
||||
# 🔧 IMPORTANT FIX: allow Git to access /src safely
|
||||
configure_git_safe_directory()
|
||||
|
||||
# Debug before cleanup
|
||||
nix_profile_list_debug("BEFORE CLEANUP")
|
||||
|
||||
# Cleanup: aggressively try to drop any pkgmgr/profile entries
|
||||
# Cleanup: drop any pkgmgr entries from nix profile
|
||||
remove_pkgmgr_from_nix_profile()
|
||||
|
||||
# Debug after cleanup
|
||||
nix_profile_list_debug("AFTER CLEANUP")
|
||||
|
||||
# Prepare argv for module execution
|
||||
sys.argv = [
|
||||
"python",
|
||||
"install",
|
||||
@@ -126,15 +138,15 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||
"--no-verification",
|
||||
]
|
||||
|
||||
# Run installation via main.py
|
||||
# Execute installation via main.py
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
|
||||
# After successful installation: run `pkgmgr --help` for debug
|
||||
# Debug: interactive shell test
|
||||
pkgmgr_help_debug()
|
||||
|
||||
finally:
|
||||
# Restore system state
|
||||
sys.argv = original_argv
|
||||
# Restore full environment
|
||||
os.environ.clear()
|
||||
os.environ.update(original_environ)
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# tests/unit/pkgmgr/installers/os_packages/test_debian_control.py
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.repository.install.context import RepoContext
|
||||
from pkgmgr.actions.repository.install.installers.os_packages.debian_control import DebianControlInstaller
|
||||
from pkgmgr.actions.repository.install.installers.os_packages.debian_control import (
|
||||
DebianControlInstaller,
|
||||
)
|
||||
|
||||
|
||||
class TestDebianControlInstaller(unittest.TestCase):
|
||||
@@ -29,14 +28,24 @@ class TestDebianControlInstaller(unittest.TestCase):
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("shutil.which", return_value="/usr/bin/dpkg-buildpackage")
|
||||
def test_supports_true(self, mock_which, mock_exists):
|
||||
"""
|
||||
supports() should return True when dpkg-buildpackage is available
|
||||
and a debian/control file exists in the repository.
|
||||
"""
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("shutil.which", return_value=None)
|
||||
def test_supports_false_without_dpkg_buildpackage(self, mock_which, mock_exists):
|
||||
"""
|
||||
supports() should return False when dpkg-buildpackage is not available,
|
||||
even if a debian/control file exists.
|
||||
"""
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("pkgmgr.actions.repository.install.installers.os_packages.debian_control.run_command")
|
||||
@patch(
|
||||
"pkgmgr.actions.repository.install.installers.os_packages.debian_control.run_command"
|
||||
)
|
||||
@patch("glob.glob", return_value=["/tmp/package-manager_0.1.1_all.deb"])
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("shutil.which")
|
||||
@@ -47,7 +56,19 @@ class TestDebianControlInstaller(unittest.TestCase):
|
||||
mock_glob,
|
||||
mock_run_command,
|
||||
):
|
||||
# dpkg-buildpackage + apt-get vorhanden
|
||||
"""
|
||||
run() should:
|
||||
|
||||
1. Install build dependencies (apt-get build-dep).
|
||||
2. Build the package using dpkg-buildpackage -b -us -uc.
|
||||
3. Discover built .deb files via glob.
|
||||
4. Install the resulting .deb packages using a suitable tool:
|
||||
- dpkg -i
|
||||
- sudo dpkg -i
|
||||
- or sudo apt-get install -y
|
||||
"""
|
||||
|
||||
# Simulate dpkg-buildpackage and apt-get being available.
|
||||
def which_side_effect(name):
|
||||
if name == "dpkg-buildpackage":
|
||||
return "/usr/bin/dpkg-buildpackage"
|
||||
@@ -64,16 +85,35 @@ class TestDebianControlInstaller(unittest.TestCase):
|
||||
# 1) apt-get update
|
||||
self.assertTrue(any("apt-get update" in cmd for cmd in cmds))
|
||||
|
||||
# 2) apt-get build-dep ./
|
||||
self.assertTrue(any("apt-get build-dep -y ./ " in cmd or
|
||||
"apt-get build-dep -y ./"
|
||||
in cmd for cmd in cmds))
|
||||
# 2) apt-get build-dep -y ./ (with or without trailing space)
|
||||
self.assertTrue(
|
||||
any(
|
||||
"apt-get build-dep -y ./ " in cmd
|
||||
or "apt-get build-dep -y ./"
|
||||
in cmd
|
||||
for cmd in cmds
|
||||
)
|
||||
)
|
||||
|
||||
# 3) dpkg-buildpackage -b -us -uc
|
||||
self.assertTrue(any("dpkg-buildpackage -b -us -uc" in cmd for cmd in cmds))
|
||||
|
||||
# 4) dpkg -i ../*.deb
|
||||
self.assertTrue(any(cmd.startswith("sudo dpkg -i ") for cmd in cmds))
|
||||
# 4) final installation of .deb packages:
|
||||
# accept dpkg -i, sudo dpkg -i, or sudo apt-get install -y
|
||||
has_plain_dpkg_install = any(cmd.startswith("dpkg -i ") for cmd in cmds)
|
||||
has_sudo_dpkg_install = any(cmd.startswith("sudo dpkg -i ") for cmd in cmds)
|
||||
has_apt_install = any(
|
||||
cmd.startswith("sudo apt-get install -y ") for cmd in cmds
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
has_plain_dpkg_install or has_sudo_dpkg_install or has_apt_install,
|
||||
msg=(
|
||||
"Expected one of 'dpkg -i', 'sudo dpkg -i' or "
|
||||
"'sudo apt-get install -y', but got commands: "
|
||||
f"{cmds}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# tests/unit/pkgmgr/installers/os_packages/test_rpm_spec.py
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.repository.install.context import RepoContext
|
||||
from pkgmgr.actions.repository.install.installers.os_packages.rpm_spec import RpmSpecInstaller
|
||||
from pkgmgr.actions.repository.install.installers.os_packages.rpm_spec import (
|
||||
RpmSpecInstaller,
|
||||
)
|
||||
|
||||
|
||||
class TestRpmSpecInstaller(unittest.TestCase):
|
||||
@@ -28,6 +28,13 @@ class TestRpmSpecInstaller(unittest.TestCase):
|
||||
@patch("glob.glob", return_value=["/tmp/repo/test.spec"])
|
||||
@patch("shutil.which")
|
||||
def test_supports_true(self, mock_which, mock_glob):
|
||||
"""
|
||||
supports() should return True when:
|
||||
- rpmbuild is available, and
|
||||
- at least one of dnf/yum/yum-builddep is available, and
|
||||
- a *.spec file is present in the repo.
|
||||
"""
|
||||
|
||||
def which_side_effect(name):
|
||||
if name == "rpmbuild":
|
||||
return "/usr/bin/rpmbuild"
|
||||
@@ -42,9 +49,14 @@ class TestRpmSpecInstaller(unittest.TestCase):
|
||||
@patch("glob.glob", return_value=[])
|
||||
@patch("shutil.which")
|
||||
def test_supports_false_missing_spec(self, mock_which, mock_glob):
|
||||
"""
|
||||
supports() should return False if no *.spec file is found,
|
||||
even if rpmbuild is present.
|
||||
"""
|
||||
mock_which.return_value = "/usr/bin/rpmbuild"
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch.object(RpmSpecInstaller, "_prepare_source_tarball")
|
||||
@patch("pkgmgr.actions.repository.install.installers.os_packages.rpm_spec.run_command")
|
||||
@patch("glob.glob")
|
||||
@patch("shutil.which")
|
||||
@@ -53,8 +65,20 @@ class TestRpmSpecInstaller(unittest.TestCase):
|
||||
mock_which,
|
||||
mock_glob,
|
||||
mock_run_command,
|
||||
mock_prepare_source_tarball,
|
||||
):
|
||||
# glob.glob wird zweimal benutzt: einmal für *.spec, einmal für gebaute RPMs
|
||||
"""
|
||||
run() should:
|
||||
|
||||
1. Determine the .spec file in the repo.
|
||||
2. Call _prepare_source_tarball() once with ctx and spec path.
|
||||
3. Install build dependencies via dnf/yum-builddep/yum.
|
||||
4. Call rpmbuild -ba <spec>.
|
||||
5. Find built RPMs via glob.
|
||||
6. Install built RPMs via dnf/yum/rpm (here: dnf).
|
||||
"""
|
||||
|
||||
# glob.glob is used twice: once for *.spec, once for built RPMs.
|
||||
def glob_side_effect(pattern, recursive=False):
|
||||
if pattern.endswith("*.spec"):
|
||||
return ["/tmp/repo/package-manager.spec"]
|
||||
@@ -77,16 +101,23 @@ class TestRpmSpecInstaller(unittest.TestCase):
|
||||
|
||||
self.installer.run(self.ctx)
|
||||
|
||||
# _prepare_source_tarball must have been called with the resolved spec path.
|
||||
mock_prepare_source_tarball.assert_called_once_with(
|
||||
self.ctx,
|
||||
"/tmp/repo/package-manager.spec",
|
||||
)
|
||||
|
||||
# Collect all command strings passed to run_command.
|
||||
cmds = [c[0][0] for c in mock_run_command.call_args_list]
|
||||
|
||||
# 1) builddep
|
||||
# 1) build dependencies (dnf builddep)
|
||||
self.assertTrue(any("builddep -y" in cmd for cmd in cmds))
|
||||
|
||||
# 2) rpmbuild -ba
|
||||
# 2) rpmbuild -ba <spec>
|
||||
self.assertTrue(any(cmd.startswith("rpmbuild -ba ") for cmd in cmds))
|
||||
|
||||
# 3) rpm -i …
|
||||
self.assertTrue(any(cmd.startswith("sudo rpm -i ") for cmd in cmds))
|
||||
# 3) installation via dnf: "sudo dnf install -y <rpms>"
|
||||
self.assertTrue(any(cmd.startswith("sudo dnf install -y ") for cmd in cmds))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -28,14 +28,27 @@ class TestNixFlakeInstaller(unittest.TestCase):
|
||||
@patch("shutil.which", return_value="/usr/bin/nix")
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_supports_true_when_nix_and_flake_exist(self, mock_exists, mock_which):
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
"""
|
||||
supports() should return True when:
|
||||
- nix is available,
|
||||
- flake.nix exists in the repo,
|
||||
- and we are not inside a Nix dev shell.
|
||||
"""
|
||||
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
|
||||
mock_which.assert_called_with("nix")
|
||||
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "flake.nix"))
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_supports_false_when_nix_missing(self, mock_exists, mock_which):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
"""
|
||||
supports() should return False if nix is not available,
|
||||
even if a flake.nix file exists.
|
||||
"""
|
||||
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("shutil.which", return_value="/usr/bin/nix")
|
||||
@@ -47,10 +60,12 @@ class TestNixFlakeInstaller(unittest.TestCase):
|
||||
mock_exists,
|
||||
):
|
||||
"""
|
||||
Ensure that run():
|
||||
- first tries to remove the old 'package-manager' profile entry
|
||||
- then installs both 'pkgmgr' and 'default' outputs.
|
||||
run() should:
|
||||
|
||||
1. attempt to remove the old 'package-manager' profile entry, and
|
||||
2. install both 'pkgmgr' and 'default' flake outputs.
|
||||
"""
|
||||
|
||||
cmds = []
|
||||
|
||||
def side_effect(cmd, cwd=None, preview=False, *args, **kwargs):
|
||||
@@ -59,18 +74,24 @@ class TestNixFlakeInstaller(unittest.TestCase):
|
||||
|
||||
mock_run_command.side_effect = side_effect
|
||||
|
||||
self.installer.run(self.ctx)
|
||||
# Simulate a normal environment (not inside nix develop, installer enabled).
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"IN_NIX_SHELL": "", "PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""},
|
||||
clear=False,
|
||||
):
|
||||
self.installer.run(self.ctx)
|
||||
|
||||
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true"
|
||||
install_pkgmgr_cmd = f"nix profile install {self.ctx.repo_dir}#pkgmgr"
|
||||
install_default_cmd = f"nix profile install {self.ctx.repo_dir}#default"
|
||||
|
||||
# Mindestens diese drei Kommandos müssen aufgerufen worden sein
|
||||
# At least these three commands must have been issued.
|
||||
self.assertIn(remove_cmd, cmds)
|
||||
self.assertIn(install_pkgmgr_cmd, cmds)
|
||||
self.assertIn(install_default_cmd, cmds)
|
||||
|
||||
# Optional: sicherstellen, dass der remove-Aufruf zuerst kam
|
||||
# Optional: ensure the remove call came first.
|
||||
self.assertEqual(cmds[0], remove_cmd)
|
||||
|
||||
@patch("shutil.which", return_value="/usr/bin/nix")
|
||||
@@ -90,8 +111,13 @@ class TestNixFlakeInstaller(unittest.TestCase):
|
||||
|
||||
mock_run_command.side_effect = side_effect
|
||||
|
||||
# Should not raise, SystemExit is swallowed internally.
|
||||
self.installer._ensure_old_profile_removed(self.ctx)
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"IN_NIX_SHELL": "", "PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""},
|
||||
clear=False,
|
||||
):
|
||||
# Should not raise, SystemExit is swallowed internally.
|
||||
self.installer._ensure_old_profile_removed(self.ctx)
|
||||
|
||||
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true"
|
||||
mock_run_command.assert_called_with(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# tests/unit/pkgmgr/installers/test_python_installer.py
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -28,18 +26,40 @@ class TestPythonInstaller(unittest.TestCase):
|
||||
|
||||
@patch("os.path.exists", side_effect=lambda path: path.endswith("pyproject.toml"))
|
||||
def test_supports_true_when_pyproject_exists(self, mock_exists):
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
"""
|
||||
supports() should return True when a pyproject.toml exists in the repo
|
||||
and we are not inside a Nix dev shell.
|
||||
"""
|
||||
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("os.path.exists", return_value=False)
|
||||
def test_supports_false_when_no_pyproject(self, mock_exists):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
"""
|
||||
supports() should return False when no pyproject.toml exists.
|
||||
"""
|
||||
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("pkgmgr.actions.repository.install.installers.python.run_command")
|
||||
@patch("os.path.exists", side_effect=lambda path: path.endswith("pyproject.toml"))
|
||||
def test_run_installs_project_from_pyproject(self, mock_exists, mock_run_command):
|
||||
self.installer.run(self.ctx)
|
||||
"""
|
||||
run() should invoke pip to install the project from pyproject.toml
|
||||
when we are not inside a Nix dev shell.
|
||||
"""
|
||||
# Simulate a normal environment (not inside nix develop).
|
||||
with patch.dict(os.environ, {"IN_NIX_SHELL": ""}, clear=False):
|
||||
self.installer.run(self.ctx)
|
||||
|
||||
# Ensure run_command was actually called.
|
||||
mock_run_command.assert_called()
|
||||
|
||||
# Extract the command string.
|
||||
cmd = mock_run_command.call_args[0][0]
|
||||
self.assertIn("pip install .", cmd)
|
||||
|
||||
# Ensure the working directory is the repo dir.
|
||||
self.assertEqual(
|
||||
mock_run_command.call_args[1].get("cwd"),
|
||||
self.ctx.repo_dir,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.branch import open_branch
|
||||
@@ -13,9 +12,10 @@ class TestOpenBranch(unittest.TestCase):
|
||||
def test_open_branch_with_explicit_name_and_default_base(self, mock_run_git) -> None:
|
||||
"""
|
||||
open_branch(name, base='main') should:
|
||||
- resolve base branch (prefers 'main', falls back to 'master')
|
||||
- fetch origin
|
||||
- checkout base
|
||||
- pull base
|
||||
- checkout resolved base
|
||||
- pull resolved base
|
||||
- create new branch
|
||||
- push with upstream
|
||||
"""
|
||||
@@ -25,6 +25,7 @@ class TestOpenBranch(unittest.TestCase):
|
||||
|
||||
# We expect a specific sequence of Git calls.
|
||||
expected_calls = [
|
||||
(["rev-parse", "--verify", "main"], "/repo"),
|
||||
(["fetch", "origin"], "/repo"),
|
||||
(["checkout", "main"], "/repo"),
|
||||
(["pull", "origin", "main"], "/repo"),
|
||||
@@ -50,7 +51,7 @@ class TestOpenBranch(unittest.TestCase):
|
||||
) -> None:
|
||||
"""
|
||||
If name is None/empty, open_branch should prompt via input()
|
||||
and still perform the full Git sequence.
|
||||
and still perform the full Git sequence on the resolved base.
|
||||
"""
|
||||
mock_run_git.return_value = ""
|
||||
|
||||
@@ -60,6 +61,7 @@ class TestOpenBranch(unittest.TestCase):
|
||||
mock_input.assert_called_once()
|
||||
|
||||
expected_calls = [
|
||||
(["rev-parse", "--verify", "develop"], "/repo"),
|
||||
(["fetch", "origin"], "/repo"),
|
||||
(["checkout", "develop"], "/repo"),
|
||||
(["pull", "origin", "develop"], "/repo"),
|
||||
@@ -76,15 +78,20 @@ class TestOpenBranch(unittest.TestCase):
|
||||
self.assertEqual(kwargs.get("cwd"), cwd_expected)
|
||||
|
||||
@patch("pkgmgr.actions.branch.run_git")
|
||||
def test_open_branch_raises_runtimeerror_on_git_failure(self, mock_run_git) -> None:
|
||||
def test_open_branch_raises_runtimeerror_on_fetch_failure(self, mock_run_git) -> None:
|
||||
"""
|
||||
If a GitError occurs (e.g. fetch fails), open_branch should
|
||||
raise a RuntimeError with a helpful message.
|
||||
If a GitError occurs on fetch, open_branch should raise a RuntimeError
|
||||
with a helpful message.
|
||||
"""
|
||||
|
||||
def side_effect(args, cwd="."):
|
||||
# Simulate a failure on the first call (fetch)
|
||||
raise GitError("simulated fetch failure")
|
||||
# First call: base resolution (rev-parse) should succeed
|
||||
if args == ["rev-parse", "--verify", "main"]:
|
||||
return ""
|
||||
# Second call: fetch should fail
|
||||
if args == ["fetch", "origin"]:
|
||||
raise GitError("simulated fetch failure")
|
||||
return ""
|
||||
|
||||
mock_run_git.side_effect = side_effect
|
||||
|
||||
@@ -95,6 +102,45 @@ class TestOpenBranch(unittest.TestCase):
|
||||
self.assertIn("Failed to fetch from origin", msg)
|
||||
self.assertIn("simulated fetch failure", msg)
|
||||
|
||||
@patch("pkgmgr.actions.branch.run_git")
|
||||
def test_open_branch_uses_fallback_master_if_main_missing(self, mock_run_git) -> None:
|
||||
"""
|
||||
If the preferred base (e.g. 'main') does not exist, open_branch should
|
||||
fall back to the fallback base (default: 'master').
|
||||
"""
|
||||
|
||||
def side_effect(args, cwd="."):
|
||||
# First: rev-parse main -> fails
|
||||
if args == ["rev-parse", "--verify", "main"]:
|
||||
raise GitError("main does not exist")
|
||||
# Second: rev-parse master -> succeeds
|
||||
if args == ["rev-parse", "--verify", "master"]:
|
||||
return ""
|
||||
# Then normal flow on master
|
||||
return ""
|
||||
|
||||
mock_run_git.side_effect = side_effect
|
||||
|
||||
open_branch(name="feature/fallback", base_branch="main", cwd="/repo")
|
||||
|
||||
expected_calls = [
|
||||
(["rev-parse", "--verify", "main"], "/repo"),
|
||||
(["rev-parse", "--verify", "master"], "/repo"),
|
||||
(["fetch", "origin"], "/repo"),
|
||||
(["checkout", "master"], "/repo"),
|
||||
(["pull", "origin", "master"], "/repo"),
|
||||
(["checkout", "-b", "feature/fallback"], "/repo"),
|
||||
(["push", "-u", "origin", "feature/fallback"], "/repo"),
|
||||
]
|
||||
|
||||
self.assertEqual(mock_run_git.call_count, len(expected_calls))
|
||||
for call, (args_expected, cwd_expected) in zip(
|
||||
mock_run_git.call_args_list, expected_calls
|
||||
):
|
||||
args, kwargs = call
|
||||
self.assertEqual(args[0], args_expected)
|
||||
self.assertEqual(kwargs.get("cwd"), cwd_expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user