186 lines
7.2 KiB
Python
186 lines
7.2 KiB
Python
"""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()
|