test: add unit tests and CI workflow for dockreap
- Add unittest-based unit tests for core volume logic - Add Makefile with isolated venv-based test runner - Add GitHub Actions CI workflow - Automatically mark SemVer-tagged commits as stable https://chatgpt.com/share/695107c4-f320-800f-b4ce-da953de9bb86
This commit is contained in:
43
.github/workflows/ci.yml
vendored
Normal file
43
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: CI (unit tests, stable tag)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed to create/move tags reliably
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Install package (editable)
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Mark commit as stable (move stable tag)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Move/create 'stable' tag to the same commit as the pushed SemVer tag
|
||||||
|
git tag -f stable "${GITHUB_SHA}"
|
||||||
|
git push -f origin stable
|
||||||
21
Makefile
Normal file
21
Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.ONESHELL:
|
||||||
|
SHELL := /bin/bash
|
||||||
|
.SHELLFLAGS := -euo pipefail -c
|
||||||
|
|
||||||
|
VENV ?= .venv
|
||||||
|
PY := $(VENV)/bin/python
|
||||||
|
PIP := $(VENV)/bin/pip
|
||||||
|
|
||||||
|
.PHONY: venv test clean
|
||||||
|
|
||||||
|
venv:
|
||||||
|
test -x "$(PY)" || python3 -m venv "$(VENV)"
|
||||||
|
"$(PIP)" install -U pip
|
||||||
|
"$(PIP)" install -e .
|
||||||
|
|
||||||
|
test: venv
|
||||||
|
"$(PY)" -m unittest discover -s tests/unit -p "test_*.py" -v
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf "$(VENV)" .pytest_cache .coverage
|
||||||
|
find . -type d -name "__pycache__" -print0 | xargs -0r rm -rf
|
||||||
BIN
src/dockreap/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/dockreap/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/dockreap/__pycache__/__main__.cpython-313.pyc
Normal file
BIN
src/dockreap/__pycache__/__main__.cpython-313.pyc
Normal file
Binary file not shown.
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
BIN
tests/unit/__pycache__/test_dockreap_main.cpython-313.pyc
Normal file
BIN
tests/unit/__pycache__/test_dockreap_main.cpython-313.pyc
Normal file
Binary file not shown.
69
tests/unit/test_dockreap_main.py
Normal file
69
tests/unit/test_dockreap_main.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
import dockreap.__main__ as dockreap_main
|
||||||
|
|
||||||
|
|
||||||
|
class TestDockreapMain(unittest.TestCase):
|
||||||
|
def test_get_anonymous_volumes_filters_64char_hex(self):
|
||||||
|
fake_stdout = "\n".join(
|
||||||
|
[
|
||||||
|
"short",
|
||||||
|
"nothex" * 10,
|
||||||
|
"a" * 64, # valid
|
||||||
|
"A" * 64, # invalid (uppercase not matched)
|
||||||
|
"g" * 64, # invalid (not hex)
|
||||||
|
"b" * 63, # invalid (too short)
|
||||||
|
"c" * 65, # invalid (too long)
|
||||||
|
"deadbeef" * 8, # valid 64-char hex
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = MagicMock()
|
||||||
|
completed.stdout = fake_stdout
|
||||||
|
|
||||||
|
with patch.object(dockreap_main.subprocess, "run", return_value=completed) as run_mock:
|
||||||
|
vols = dockreap_main.get_anonymous_volumes()
|
||||||
|
|
||||||
|
# Ensure docker command was called correctly
|
||||||
|
run_mock.assert_called_with(
|
||||||
|
["docker", "volume", "ls", "--format", "{{.Name}}"],
|
||||||
|
stdout=dockreap_main.subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(vols, ["a" * 64, "deadbeef" * 8])
|
||||||
|
|
||||||
|
def test_is_volume_used_true_when_docker_returns_container_ids(self):
|
||||||
|
completed = MagicMock()
|
||||||
|
completed.stdout = "abc123\n"
|
||||||
|
|
||||||
|
with patch.object(dockreap_main.subprocess, "run", return_value=completed):
|
||||||
|
self.assertTrue(dockreap_main.is_volume_used("x" * 64))
|
||||||
|
|
||||||
|
def test_is_volume_used_false_when_no_container_ids(self):
|
||||||
|
completed = MagicMock()
|
||||||
|
completed.stdout = "\n"
|
||||||
|
|
||||||
|
with patch.object(dockreap_main.subprocess, "run", return_value=completed):
|
||||||
|
self.assertFalse(dockreap_main.is_volume_used("x" * 64))
|
||||||
|
|
||||||
|
def test_cleanup_symlink_does_nothing_when_not_symlink(self):
|
||||||
|
# We don't want to touch the real filesystem, so we patch the Path object
|
||||||
|
fake_data_path = MagicMock()
|
||||||
|
fake_data_path.is_symlink.return_value = False
|
||||||
|
|
||||||
|
fake_volume_dir = MagicMock()
|
||||||
|
fake_volume_dir.__truediv__.side_effect = lambda x: fake_data_path if x == "_data" else MagicMock()
|
||||||
|
|
||||||
|
with patch.object(dockreap_main, "VOLUME_BASE_PATH") as base_path_mock:
|
||||||
|
base_path_mock.__truediv__.return_value = fake_volume_dir
|
||||||
|
|
||||||
|
# Should not raise; should not try to unlink/rmtree
|
||||||
|
dockreap_main.cleanup_symlink("a" * 64)
|
||||||
|
|
||||||
|
fake_data_path.unlink.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user