test_release.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """发布包边界与部署管理的回归测试;使用临时目录,不触碰共享数据。"""
  2. from __future__ import annotations
  3. from pathlib import Path
  4. import shutil
  5. import subprocess
  6. import sys
  7. import tarfile
  8. import pytest
  9. ROOT = Path(__file__).resolve().parents[1]
  10. sys.path.insert(0, str(ROOT / "release"))
  11. import build as release_build
  12. import common as release_common
  13. import deploy as release_deploy
  14. @pytest.fixture
  15. def source(tmp_path):
  16. root = tmp_path / "source"
  17. for original in release_build.source_files(ROOT):
  18. dest = root / original.relative_to(ROOT)
  19. dest.parent.mkdir(parents=True, exist_ok=True)
  20. shutil.copyfile(original, dest)
  21. return root
  22. @pytest.mark.parametrize("value", ["../2.0.0", "2.0", "01.2.3", "2.0.0/evil", "2.0.0+local", ""])
  23. def test_invalid_versions(value):
  24. with pytest.raises(ValueError):
  25. release_common.version(value)
  26. def test_archive_boundary_and_integrity(source, tmp_path):
  27. (source / ".env").write_text("secret")
  28. (source / "data").mkdir()
  29. (source / "data/private.sqlite").write_text("private")
  30. (source / "agents/.env").write_text("secret")
  31. archive = release_build.build(source, tmp_path / "dist", "2.1.0-rc.1", checks=False)
  32. assert archive.with_suffix(".gz.sha256").read_text().split()[0] == release_common.sha256(archive)
  33. extracted = tmp_path / "extract"
  34. with tarfile.open(archive) as tar:
  35. tar.extractall(extracted, filter="data")
  36. root = extracted / "vent-agent-2.1.0-rc.1-linux"
  37. manifest = release_common.validate_package(root)
  38. assert manifest["checks"] == "skipped"
  39. assert (root / "VERSION").read_text().strip() == "2.1.0-rc.1"
  40. assert not (root / ".env").exists()
  41. assert not (root / "data").exists()
  42. assert not (root / "agents/.env").exists()
  43. assert (source / "VERSION").read_text() != "2.1.0-rc.1\n"
  44. # 校验脚本本身不应生成清单外 __pycache__ 导致第二次校验失败。
  45. for _ in range(2):
  46. subprocess.run([sys.executable, str(root / "release/verify.py")], check=True, capture_output=True)
  47. with pytest.raises(ValueError, match="不覆盖"):
  48. release_build.build(source, tmp_path / "dist", "2.1.0-rc.1", checks=False)
  49. (root / "main.py").write_text("tampered")
  50. with pytest.raises(ValueError, match="校验失败"):
  51. release_common.validate_package(root)
  52. def test_source_symlink_rejected(source, tmp_path):
  53. (source / "agents/injected.py").symlink_to(tmp_path / "outside.py")
  54. with pytest.raises(ValueError, match="符号链接"):
  55. release_build.source_files(source)
  56. def test_stale_lock_rejected(source):
  57. with (source / "requirements.txt").open("a") as stream:
  58. stream.write("\nnew-package==1.0\n")
  59. with pytest.raises(ValueError, match="已变化"):
  60. release_build.verify_lock(source, False)
  61. def test_git_is_optional(tmp_path, monkeypatch):
  62. def missing(*args, **kwargs):
  63. raise FileNotFoundError("git")
  64. monkeypatch.setattr(release_build.subprocess, "run", missing)
  65. assert release_build.git_info(tmp_path) == {"commit": None, "dirty": None}
  66. def test_extraneous_file_rejected(source, tmp_path):
  67. archive = release_build.build(source, tmp_path / "dist", "2.0.1", checks=False)
  68. with tarfile.open(archive) as tar:
  69. tar.extractall(tmp_path / "extract", filter="data")
  70. root = tmp_path / "extract/vent-agent-2.0.1-linux"
  71. (root / ".env").write_text("unexpected")
  72. with pytest.raises(ValueError, match="清单外"):
  73. release_common.validate_package(root)
  74. release_common.validate_package(root, installed=True)
  75. def test_instance_lock_prevents_activation(tmp_path):
  76. assert not release_deploy.running(tmp_path)
  77. with release_deploy.locked(tmp_path / "shared/run/instance.lock"):
  78. assert release_deploy.running(tmp_path)
  79. with pytest.raises(ValueError, match="先 stop"):
  80. release_deploy.activate(tmp_path, "2.0.0")
  81. assert not release_deploy.running(tmp_path)
  82. def test_reused_pid_not_signalled(monkeypatch):
  83. monkeypatch.setattr(release_deploy, "start_ticks", lambda pid: "new")
  84. assert not release_deploy.same_process({"pid": 123, "start_ticks": "old"})
  85. assert not release_deploy.same_process({"pid": 1, "start_ticks": "new"})
  86. assert release_deploy.same_process({"pid": 123, "start_ticks": "new"})
  87. @pytest.fixture
  88. def package(source, tmp_path):
  89. archive = release_build.build(source, tmp_path / "dist", "2.0.1", checks=False)
  90. with tarfile.open(archive) as tar:
  91. tar.extractall(tmp_path / "extract", filter="data")
  92. return tmp_path / "extract/vent-agent-2.0.1-linux"
  93. def test_install_preserves_shared_and_activation(package, tmp_path, monkeypatch):
  94. prefix = tmp_path / "installed"
  95. shared = prefix / "shared"
  96. (shared / "data").mkdir(parents=True)
  97. (shared / ".env").write_text("existing-secret")
  98. (shared / "data/sentinel").write_text("keep")
  99. monkeypatch.setattr(release_deploy, "PACKAGE", package)
  100. def fake_run(command, **kwargs):
  101. if "venv" in command:
  102. python = Path(command[-1]) / "bin/python"
  103. python.parent.mkdir(parents=True)
  104. python.write_text("test")
  105. return subprocess.CompletedProcess(command, 0)
  106. monkeypatch.setattr(release_deploy.subprocess, "run", fake_run)
  107. release_deploy.install(prefix)
  108. target = prefix / "releases/2.0.1"
  109. assert (target / ".env").resolve() == shared / ".env"
  110. assert (target / "data").resolve() == shared / "data"
  111. assert (shared / ".env").read_text() == "existing-secret"
  112. assert (shared / "data/sentinel").read_text() == "keep"
  113. assert not (prefix / "current").exists()
  114. with pytest.raises(ValueError, match="不覆盖"):
  115. release_deploy.install(prefix)
  116. release_deploy.activate(prefix, "2.0.1")
  117. assert release_deploy.current_release(prefix) == target
  118. def test_failed_install_cleans_only_new_version(package, tmp_path, monkeypatch):
  119. prefix = tmp_path / "installed"
  120. shared = prefix / "shared"
  121. shared.mkdir(parents=True)
  122. (shared / ".env").write_text("keep")
  123. monkeypatch.setattr(release_deploy, "PACKAGE", package)
  124. def fail(command, **kwargs):
  125. raise subprocess.CalledProcessError(1, command)
  126. monkeypatch.setattr(release_deploy.subprocess, "run", fail)
  127. with pytest.raises(subprocess.CalledProcessError):
  128. release_deploy.install(prefix)
  129. assert not (prefix / "releases/2.0.1").exists()
  130. assert (shared / ".env").read_text() == "keep"