"""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()