Add Arch-based Docker test setup, shallow clone mode support and pkgmgr tests (see ChatGPT conversation: https://chatgpt.com/share/693052a1-edd0-800f-a9d6-c154b8e7d8e0)

This commit is contained in:
Kevin Veen-Birkenbach
2025-12-03 16:09:42 +01:00
parent 71cf032506
commit c4395a4764
9 changed files with 484 additions and 31 deletions

25
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Test package-manager
on:
push:
branches:
- main
- master
- develop
- "*"
pull_request:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show Docker version
run: docker version
- name: Run tests via make (builds Docker image and runs unit + integration tests)
run: make test

View File

@@ -1,31 +1,40 @@
FROM python:3.11-slim
FROM archlinux:latest
# Install system dependencies (make, pip) as per README
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
make \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Update system and install core tooling
RUN pacman -Syu --noconfirm \
&& pacman -S --noconfirm --needed \
git \
make \
sudo \
python \
python-pip \
python-virtualenv \
python-setuptools \
python-wheel \
&& pacman -Scc --noconfirm
# Ensure local bin is in PATH (for aliases) as per README
# Ensure local bin is in PATH (for pkgmgr links)
ENV PATH="/root/.local/bin:$PATH"
# Create and activate a virtual environment
# Create virtual environment
ENV VIRTUAL_ENV=/root/.venvs/pkgmgr
RUN python3 -m venv $VIRTUAL_ENV
RUN python -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Copy local package-manager source into the image
# Working directory for the package-manager project
WORKDIR /root/Repositories/github.com/kevinveenbirkenbach/package-manager
# Copy local package-manager source into container
COPY . .
# Install Python dependencies and set up the tool non-interactively
# Install Python dependencies and register pkgmgr inside the venv
RUN pip install --upgrade pip \
&& pip install PyYAML \
&& chmod +x main.py \
&& python main.py install package-manager --quiet --clone-mode https
&& python main.py install package-manager --quiet --clone-mode shallow --no-verification
# Copy again to allow rebuild-based code changes
COPY . .
# Default entrypoint for pkgmgr
ENTRYPOINT ["pkgmgr"]
CMD ["--help"]

View File

@@ -3,6 +3,10 @@
setup: install
@python3 main.py install
test:
docker build -t package-manager-test .
docker run --rm --entrypoint python package-manager-test -m unittest discover -s tests -p "test_*.py"
install:
@echo "Making 'main.py' executable..."
@chmod +x main.py

47
main.py
View File

@@ -112,10 +112,29 @@ For detailed help on each command, use:
def add_install_update_arguments(subparser):
add_identifier_arguments(subparser)
subparser.add_argument("-q", "--quiet", action="store_true", help="Suppress warnings and info messages")
subparser.add_argument("--no-verification", action="store_true", default=False, help="Disable verification via commit/gpg")
subparser.add_argument("--dependencies", action="store_true", help="Also pull and update dependencies")
subparser.add_argument("--clone-mode", choices=["ssh", "https"], default="ssh", help="Specify the clone mode (default: ssh)")
subparser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Suppress warnings and info messages",
)
subparser.add_argument(
"--no-verification",
action="store_true",
default=False,
help="Disable verification via commit/gpg",
)
subparser.add_argument(
"--dependencies",
action="store_true",
help="Also pull and update dependencies",
)
subparser.add_argument(
"--clone-mode",
choices=["ssh", "https", "shallow"],
default="ssh",
help="Specify the clone mode: ssh, https, or shallow (HTTPS shallow clone; default: ssh)",
)
install_parser = subparsers.add_parser("install", help="Setup repository/repositories alias links to executables")
add_install_update_arguments(install_parser)
@@ -213,10 +232,20 @@ For detailed help on each command, use:
description=f"Executes '{command} {subcommand}' for the identified repos.\nTo recieve more help execute '{command} {subcommand} --help'",
formatter_class=argparse.RawTextHelpFormatter
)
if subcommand in ["pull","clone"]:
proxy_command_parsers[f"{command}_{subcommand}"].add_argument("--no-verification", action="store_true", default=False, help="Disable verification via commit/gpg")
if subcommand in ["pull", "clone"]:
proxy_command_parsers[f"{command}_{subcommand}"].add_argument(
"--no-verification",
action="store_true",
default=False,
help="Disable verification via commit/gpg",
)
if subcommand == "clone":
proxy_command_parsers[f"{command}_{subcommand}"].add_argument("--clone-mode", choices=["ssh", "https"], default="ssh", help="Specify the clone mode (default: ssh)")
proxy_command_parsers[f"{command}_{subcommand}"].add_argument(
"--clone-mode",
choices=["ssh", "https", "shallow"],
default="ssh",
help="Specify the clone mode: ssh, https, or shallow (HTTPS shallow clone; default: ssh)",
)
add_identifier_arguments(proxy_command_parsers[f"{command}_{subcommand}"])
args = parser.parse_args()
@@ -331,7 +360,7 @@ For detailed help on each command, use:
status_repos(selected,REPOSITORIES_BASE_DIR, ALL_REPOSITORIES, args.extra_args, list_only=args.list, system_status=args.system, preview=args.preview)
elif args.command == "explore":
for repository in selected:
run_command(f"nautilus {repository["directory"]} & disown")
run_command(f"nautilus {repository['directory']} & disown")
elif args.command == "code":
if not selected:
print("No repositories selected.")
@@ -371,7 +400,7 @@ For detailed help on each command, use:
# Join the provided shell command parts into one string.
command_to_run = " ".join(args.shell_command)
for repository in selected:
print(f"Executing in '{repository["directory"]}': {command_to_run}")
print(f"Executing in '{repository['directory']}': {command_to_run}")
run_command(command_to_run, cwd=repository["directory"], preview=args.preview)
elif args.command == "config":
if args.subcommand == "show":

View File

@@ -22,25 +22,48 @@ def clone_repos(
parent_dir = os.path.dirname(repo_dir)
os.makedirs(parent_dir, exist_ok=True)
# Build clone URL based on the clone_mode
# Build clone URL based on the clone_mode
if clone_mode == "ssh":
clone_url = f"git@{repo.get('provider')}:{repo.get('account')}/{repo.get('repository')}.git"
elif clone_mode == "https":
clone_url = (
f"git@{repo.get('provider')}:"
f"{repo.get('account')}/"
f"{repo.get('repository')}.git"
)
elif clone_mode in ("https", "shallow"):
# Use replacement if defined, otherwise construct from provider/account/repository
if repo.get("replacement"):
clone_url = f"https://{repo.get('replacement')}.git"
else:
clone_url = f"https://{repo.get('provider')}/{repo.get('account')}/{repo.get('repository')}.git"
clone_url = (
f"https://{repo.get('provider')}/"
f"{repo.get('account')}/"
f"{repo.get('repository')}.git"
)
else:
print(f"Unknown clone mode '{clone_mode}'. Aborting clone for {repo_identifier}.")
continue
print(f"[INFO] Attempting to clone '{repo_identifier}' using {clone_mode.upper()} from {clone_url} into '{repo_dir}'.")
# Build base clone command
base_clone_cmd = "git clone"
if clone_mode == "shallow":
# Shallow clone: only latest state via HTTPS, no full history
base_clone_cmd += " --depth 1 --single-branch"
mode_label = "HTTPS (shallow)" if clone_mode == "shallow" else clone_mode.upper()
print(
f"[INFO] Attempting to clone '{repo_identifier}' using {mode_label} "
f"from {clone_url} into '{repo_dir}'."
)
if preview:
print(f"[Preview] Would run: git clone {clone_url} {repo_dir} in {parent_dir}")
print(f"[Preview] Would run: {base_clone_cmd} {clone_url} {repo_dir} in {parent_dir}")
result = subprocess.CompletedProcess(args=[], returncode=0)
else:
result = subprocess.run(f"git clone {clone_url} {repo_dir}", cwd=parent_dir, shell=True)
result = subprocess.run(
f"{base_clone_cmd} {clone_url} {repo_dir}",
cwd=parent_dir,
shell=True,
)
if result.returncode != 0:
# Only offer fallback if the original mode was SSH.

168
tests/test_clone_repos.py Normal file
View File

@@ -0,0 +1,168 @@
# tests/test_clone_repos.py
import unittest
from unittest.mock import patch, MagicMock
from pkgmgr.clone_repos import clone_repos
class TestCloneRepos(unittest.TestCase):
def setUp(self):
self.repo = {
"provider": "github.com",
"account": "user",
"repository": "repo",
}
self.selected = [self.repo]
self.base_dir = "/tmp/repos"
self.all_repos = self.selected
@patch("pkgmgr.clone_repos.verify_repository")
@patch("pkgmgr.clone_repos.subprocess.run")
@patch("pkgmgr.clone_repos.os.makedirs")
@patch("pkgmgr.clone_repos.os.path.exists")
@patch("pkgmgr.clone_repos.get_repo_dir")
@patch("pkgmgr.clone_repos.get_repo_identifier")
def test_clone_ssh_mode_uses_ssh_url(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_exists,
mock_makedirs,
mock_run,
mock_verify,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
mock_exists.return_value = False
mock_run.return_value = MagicMock(returncode=0)
mock_verify.return_value = (True, [], "hash", "key")
clone_repos(
self.selected,
self.base_dir,
self.all_repos,
preview=False,
no_verification=True,
clone_mode="ssh",
)
mock_run.assert_called_once()
# subprocess.run wird mit positional args aufgerufen
cmd = mock_run.call_args[0][0]
cwd = mock_run.call_args[1]["cwd"]
self.assertIn("git clone", cmd)
self.assertIn("git@github.com:user/repo.git", cmd)
self.assertEqual(cwd, "/tmp/repos/user")
@patch("pkgmgr.clone_repos.verify_repository")
@patch("pkgmgr.clone_repos.subprocess.run")
@patch("pkgmgr.clone_repos.os.makedirs")
@patch("pkgmgr.clone_repos.os.path.exists")
@patch("pkgmgr.clone_repos.get_repo_dir")
@patch("pkgmgr.clone_repos.get_repo_identifier")
def test_clone_https_mode_uses_https_url(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_exists,
mock_makedirs,
mock_run,
mock_verify,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
mock_exists.return_value = False
mock_run.return_value = MagicMock(returncode=0)
mock_verify.return_value = (True, [], "hash", "key")
clone_repos(
self.selected,
self.base_dir,
self.all_repos,
preview=False,
no_verification=True,
clone_mode="https",
)
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
cwd = mock_run.call_args[1]["cwd"]
self.assertIn("git clone", cmd)
self.assertIn("https://github.com/user/repo.git", cmd)
self.assertEqual(cwd, "/tmp/repos/user")
@patch("pkgmgr.clone_repos.verify_repository")
@patch("pkgmgr.clone_repos.subprocess.run")
@patch("pkgmgr.clone_repos.os.makedirs")
@patch("pkgmgr.clone_repos.os.path.exists")
@patch("pkgmgr.clone_repos.get_repo_dir")
@patch("pkgmgr.clone_repos.get_repo_identifier")
def test_clone_shallow_mode_uses_https_with_depth(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_exists,
mock_makedirs,
mock_run,
mock_verify,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
mock_exists.return_value = False
mock_run.return_value = MagicMock(returncode=0)
mock_verify.return_value = (True, [], "hash", "key")
clone_repos(
self.selected,
self.base_dir,
self.all_repos,
preview=False,
no_verification=True,
clone_mode="shallow",
)
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
cwd = mock_run.call_args[1]["cwd"]
self.assertIn("git clone --depth 1 --single-branch", cmd)
self.assertIn("https://github.com/user/repo.git", cmd)
self.assertEqual(cwd, "/tmp/repos/user")
@patch("pkgmgr.clone_repos.verify_repository")
@patch("pkgmgr.clone_repos.subprocess.run")
@patch("pkgmgr.clone_repos.os.makedirs")
@patch("pkgmgr.clone_repos.os.path.exists")
@patch("pkgmgr.clone_repos.get_repo_dir")
@patch("pkgmgr.clone_repos.get_repo_identifier")
def test_preview_mode_does_not_call_subprocess_run(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_exists,
mock_makedirs,
mock_run,
mock_verify,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
mock_exists.return_value = False
mock_verify.return_value = (True, [], "hash", "key")
clone_repos(
self.selected,
self.base_dir,
self.all_repos,
preview=True,
no_verification=True,
clone_mode="shallow",
)
# Im Preview-Modus sollte subprocess.run nicht aufgerufen werden
mock_run.assert_not_called()
if __name__ == "__main__":
unittest.main()

129
tests/test_install_repos.py Normal file
View File

@@ -0,0 +1,129 @@
# tests/test_install_repos.py
import os
import unittest
from unittest.mock import patch, MagicMock, mock_open
from pkgmgr.install_repos import install_repos
class TestInstallRepos(unittest.TestCase):
def setUp(self):
self.repo = {
"provider": "github.com",
"account": "user",
"repository": "repo",
}
self.selected = [self.repo]
self.base_dir = "/tmp/repos"
self.bin_dir = "/tmp/bin"
self.all_repos = self.selected
@patch("pkgmgr.install_repos.clone_repos")
@patch("pkgmgr.install_repos.os.path.exists")
@patch("pkgmgr.install_repos.get_repo_dir")
@patch("pkgmgr.install_repos.get_repo_identifier")
def test_calls_clone_repos_with_clone_mode(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_exists,
mock_clone_repos,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
# Repo-Verzeichnis existiert nicht -> soll geklont werden
mock_exists.return_value = False
install_repos(
self.selected,
self.base_dir,
self.bin_dir,
self.all_repos,
no_verification=True,
preview=False,
quiet=True,
clone_mode="shallow",
update_dependencies=False,
)
mock_clone_repos.assert_called_once()
args, kwargs = mock_clone_repos.call_args
# clone_mode ist letztes Argument
self.assertEqual(args[-1], "shallow")
@patch("pkgmgr.install_repos.run_command")
@patch("pkgmgr.install_repos.open", new_callable=mock_open, create=True)
@patch("pkgmgr.install_repos.yaml.safe_load")
@patch("pkgmgr.install_repos.os.path.exists")
@patch("pkgmgr.install_repos.create_ink")
@patch("pkgmgr.install_repos.verify_repository")
@patch("pkgmgr.install_repos.get_repo_dir")
@patch("pkgmgr.install_repos.get_repo_identifier")
def test_pkgmgr_requirements_propagate_clone_mode(
self,
mock_get_repo_identifier,
mock_get_repo_dir,
mock_verify,
mock_create_ink,
mock_exists,
mock_safe_load,
mock_open_file,
mock_run_command,
):
mock_get_repo_identifier.return_value = "github.com/user/repo"
repo_dir = "/tmp/repos/user/repo"
mock_get_repo_dir.return_value = repo_dir
# exists() muss True für repo_dir & requirements.yml liefern,
# sonst werden die Anforderungen nie verarbeitet.
def exists_side_effect(path):
if path == repo_dir:
return True
if path == os.path.join(repo_dir, "requirements.yml"):
return True
# requirements.txt und Makefile sollen "nicht existieren"
return False
mock_exists.side_effect = exists_side_effect
mock_verify.return_value = (True, [], "hash", "key")
# requirements.yml enthält pkgmgr-Dependencies
mock_safe_load.return_value = {
"pkgmgr": ["github.com/other/account/dep"],
}
commands = []
def run_command_side_effect(cmd, cwd=None, preview=False):
commands.append((cmd, cwd, preview))
mock_run_command.side_effect = run_command_side_effect
install_repos(
self.selected,
self.base_dir,
self.bin_dir,
self.all_repos,
no_verification=False,
preview=False,
quiet=True,
clone_mode="shallow",
update_dependencies=False,
)
# Prüfen, dass ein pkgmgr install Befehl mit --clone-mode shallow gebaut wurde
pkgmgr_install_cmds = [
c for (c, cwd, preview) in commands if "pkgmgr install" in c
]
self.assertTrue(
pkgmgr_install_cmds,
f"No pkgmgr install command was executed. Commands seen: {commands}",
)
cmd = pkgmgr_install_cmds[0]
self.assertIn("--clone-mode shallow", cmd)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,47 @@
# tests/test_integration_install_all_shallow.py
"""
Integration test: install all configured repositories using
--clone-mode shallow (HTTPS shallow clone) and --no-verification.
This test is intended to be run inside the Docker container where:
- network access is available,
- the config/config.yaml is present,
- and it is safe to perform real git operations.
It passes if the command completes without raising an exception.
"""
import runpy
import sys
import unittest
class TestIntegrationInstallAllShallow(unittest.TestCase):
def test_install_all_repositories_shallow(self):
"""
Run: pkgmgr install --all --clone-mode shallow --no-verification
This will perform real installations/clones inside the container.
The test succeeds if no exception is raised.
"""
original_argv = sys.argv
try:
sys.argv = [
"pkgmgr",
"install",
"--all",
"--clone-mode",
"shallow",
"--no-verification",
]
# Execute main.py as if it was called from CLI.
# This will run the full install pipeline inside the container.
runpy.run_module("main", run_name="__main__")
finally:
sys.argv = original_argv
if __name__ == "__main__":
unittest.main()

19
tests/test_main.py Normal file
View File

@@ -0,0 +1,19 @@
# tests/test_main.py
import unittest
import main
class TestMainModule(unittest.TestCase):
def test_proxy_commands_defined(self):
"""
Basic sanity check: main.py should define PROXY_COMMANDS
with git/docker/docker compose entries.
"""
self.assertTrue(hasattr(main, "PROXY_COMMANDS"))
self.assertIn("git", main.PROXY_COMMANDS)
self.assertIn("docker", main.PROXY_COMMANDS)
self.assertIn("docker compose", main.PROXY_COMMANDS)
if __name__ == "__main__":
unittest.main()