CI: build pipeline testet nun immer automatisch vor dem build alle Test. Ausserdem gibts einen deploy-pfad

This commit is contained in:
Rob committed 2026-10-07 20:44:51 +02:00
1 parent 971459c9bf
commit 2aad2c9d99
12 files changed
+543 -27

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
"""Verify pipeline gates and deployment against temporary local directories."""
import importlib.util
from pathlib import Path
import sys
from unittest.mock import patch, Mock
import zipfile
import pytest
PIPELINE_DIR = Path(__file__).resolve().parents[2] / "1_build-pipeline"
sys.path.insert(0, str(PIPELINE_DIR))
import deploy
spec = importlib.util.spec_from_file_location("build_pipeline", PIPELINE_DIR / "build.py")
build = importlib.util.module_from_spec(spec)
spec.loader.exec_module(build)
@pytest.fixture
def release(tmp_path):
directory = tmp_path / "build"
directory.mkdir()
for name in deploy.PACKAGE_FILES:
path = directory / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"test release payload")
deploy.write_release_manifest(directory, "1.3")
target = tmp_path / "share"
target.mkdir()
return directory, target
def test_deployment_packages_release_without_work_files(release):
directory, target = release
for folder in ["input", "output"]:
(directory / folder).mkdir()
(directory / folder / "private.xlsx").write_bytes(b"private")
result = deploy.deploy(directory, target)
assert result.name == "Tätigkeitsnachweis-Generator v1.3.0.zip"
with zipfile.ZipFile(result) as archive:
assert set(archive.namelist()) == set(deploy.PACKAGE_FILES + ["input/", "output/"])
assert archive.testzip() is None
assert not list(target.glob(".upload-*"))
# An unchanged release can be retried without overwriting the published ZIP.
original_time = result.stat().st_mtime_ns
assert deploy.deploy(directory, target) == result
assert result.stat().st_mtime_ns == original_time
def test_missing_vpn_target_is_not_created(release):
directory, target = release
missing = target / "missing-share"
with pytest.raises(OSError, match="VPN"):
deploy.deploy(directory, missing)
assert not missing.exists()
assert not list(directory.glob("*.zip"))
def test_changed_payload_is_not_deployed(release):
directory, target = release
(directory / deploy.EXE_NAME).write_bytes(b"different executable")
with pytest.raises(ValueError, match="verändert"):
deploy.deploy(directory, target)
assert not list(target.iterdir())
def test_unconfirmed_old_build_is_not_deployed(release):
directory, target = release
(directory / deploy.MANIFEST_NAME).unlink()
with pytest.raises(ValueError, match="bestätigter Build"):
deploy.deploy(directory, target)
def test_existing_different_release_is_preserved(release):
directory, target = release
existing = target / "Tätigkeitsnachweis-Generator v1.3.0.zip"
existing.write_bytes(b"already published")
with pytest.raises(FileExistsError, match="anderem Inhalt"):
deploy.deploy(directory, target)
assert existing.read_bytes() == b"already published"
def test_interrupted_upload_is_not_published(release):
directory, target = release
with patch.object(deploy.shutil, "copyfile", side_effect=OSError("VPN disconnected")):
with pytest.raises(OSError, match="lokale Build bleibt erhalten"):
deploy.deploy(directory, target)
assert not list(target.iterdir())
assert list(directory.glob("*.zip"))
assert deploy.validate_release(directory) == "1.3.0"
def test_corrupt_upload_is_not_published(release):
directory, target = release
def corrupt_copy(source, destination):
Path(destination).write_bytes(b"corrupted")
with patch.object(deploy.shutil, "copyfile", side_effect=corrupt_copy):
with pytest.raises(OSError):
deploy.deploy(directory, target)
assert not list(target.iterdir())
@pytest.fixture
def pipeline(tmp_path, monkeypatch):
changelog = tmp_path / "CHANGELOG.md"
changelog.write_text("## [1.3]\n\nOriginal release\n", encoding="utf-8")
version_file = tmp_path / ".last_version"
version_file.write_text("1.3", encoding="utf-8")
monkeypatch.setattr(build, "CHANGELOG_PATH", changelog)
monkeypatch.setattr(build, "VERSION_FILE", version_file)
monkeypatch.setattr(build, "BUILD_OUTPUT_DIR", tmp_path)
monkeypatch.setattr(sys, "argv", ["build.py", "--bump", "patch", "--message", "Fix"])
events = []
names = ["_run_tests", "_ensure_pyinstaller", "_prepare_build_dir", "_run_pyinstaller",
"_copy_to_build", "write_release_manifest", "_cleanup", "_print_summary", "deploy"]
mocks = {}
for name in names:
mocks[name] = Mock(side_effect=lambda *args, _name=name: events.append(_name))
monkeypatch.setattr(build, name, mocks[name])
return changelog, version_file, events, mocks
def test_pipeline_tests_build_then_deploy(pipeline):
changelog, version_file, events, mocks = pipeline
build.main()
assert events == ["_run_tests", "_ensure_pyinstaller", "_prepare_build_dir", "_run_pyinstaller",
"_copy_to_build", "write_release_manifest", "_cleanup", "_print_summary", "deploy"]
assert version_file.read_text() == "1.3.1"
assert "## [1.3.1]" in changelog.read_text(encoding="utf-8")
def test_failing_tests_prevent_version_change_build_and_deploy(pipeline):
changelog, version_file, events, mocks = pipeline
original = changelog.read_bytes()
mocks["_run_tests"].side_effect = SystemExit(1)
with pytest.raises(SystemExit):
build.main()
assert changelog.read_bytes() == original
assert version_file.read_text() == "1.3"
mocks["_run_pyinstaller"].assert_not_called()
mocks["deploy"].assert_not_called()
def test_build_failure_rolls_back_changelog_and_prevents_deployment(pipeline):
changelog, version_file, events, mocks = pipeline
original = changelog.read_text(encoding="utf-8")
mocks["_run_pyinstaller"].side_effect = SystemExit(1)
with pytest.raises(SystemExit):
build.main()
assert changelog.read_text(encoding="utf-8") == original
assert version_file.read_text() == "1.3"
mocks["deploy"].assert_not_called()
mocks["_cleanup"].assert_called_once()
def test_deploy_failure_keeps_successful_build_version(pipeline):
changelog, version_file, events, mocks = pipeline
mocks["deploy"].side_effect = OSError("VPN unavailable")
with pytest.raises(SystemExit):
build.main()
assert version_file.read_text() == "1.3.1"
assert "## [1.3.1]" in changelog.read_text(encoding="utf-8")
mocks["write_release_manifest"].assert_called_once()
def test_dry_run_does_not_test_build_or_deploy(pipeline, monkeypatch):
changelog, version_file, events, mocks = pipeline
monkeypatch.setattr(sys, "argv", ["build.py", "--bump", "patch", "--dry-run"])
build.main()
assert not events
assert version_file.read_text() == "1.3"
def test_rebuild_preserves_changelog_and_version(pipeline, monkeypatch):
changelog, version_file, events, mocks = pipeline
original = changelog.read_bytes()
monkeypatch.setattr(sys, "argv", ["build.py", "--no-bump", "--no-deploy"])
build.main()
assert changelog.read_bytes() == original
assert version_file.read_text() == "1.3"
mocks["write_release_manifest"].assert_called_once_with(build.BUILD_OUTPUT_DIR, "1.3")
mocks["_run_tests"].assert_called_once()
mocks["_run_pyinstaller"].assert_called_once()
mocks["deploy"].assert_not_called()