test_release.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 (root / "algorithms/characteristic_curve_recognition/service.py").is_file()
  44. assert (root / "algorithms/characteristic_curve_recognition/vendor/chart_axis_detector/current_image_pipeline.py").is_file()
  45. assert (source / "VERSION").read_text() != "2.1.0-rc.1\n"
  46. # 校验脚本本身不应生成清单外 __pycache__ 导致第二次校验失败。
  47. for _ in range(2):
  48. subprocess.run([sys.executable, str(root / "release/verify.py")], check=True, capture_output=True)
  49. with pytest.raises(ValueError, match="不覆盖"):
  50. release_build.build(source, tmp_path / "dist", "2.1.0-rc.1", checks=False)
  51. (root / "main.py").write_text("tampered")
  52. with pytest.raises(ValueError, match="校验失败"):
  53. release_common.validate_package(root)
  54. def test_source_symlink_rejected(source, tmp_path):
  55. (source / "agents/injected.py").symlink_to(tmp_path / "outside.py")
  56. with pytest.raises(ValueError, match="符号链接"):
  57. release_build.source_files(source)
  58. def test_stale_lock_rejected(source):
  59. with (source / "requirements.txt").open("a") as stream:
  60. stream.write("\nnew-package==1.0\n")
  61. with pytest.raises(ValueError, match="已变化"):
  62. release_build.verify_lock(source, False)
  63. def test_git_is_optional(tmp_path, monkeypatch):
  64. def missing(*args, **kwargs):
  65. raise FileNotFoundError("git")
  66. monkeypatch.setattr(release_build.subprocess, "run", missing)
  67. assert release_build.git_info(tmp_path) == {"commit": None, "dirty": None}
  68. def test_extraneous_file_rejected(source, tmp_path):
  69. archive = release_build.build(source, tmp_path / "dist", "2.0.1", checks=False)
  70. with tarfile.open(archive) as tar:
  71. tar.extractall(tmp_path / "extract", filter="data")
  72. root = tmp_path / "extract/vent-agent-2.0.1-linux"
  73. (root / ".env").write_text("unexpected")
  74. with pytest.raises(ValueError, match="清单外"):
  75. release_common.validate_package(root)
  76. release_common.validate_package(root, installed=True)
  77. def test_instance_lock_prevents_activation(tmp_path):
  78. assert not release_deploy.running(tmp_path)
  79. with release_deploy.locked(tmp_path / "shared/run/instance.lock"):
  80. assert release_deploy.running(tmp_path)
  81. with pytest.raises(ValueError, match="先 stop"):
  82. release_deploy.activate(tmp_path, "2.0.0")
  83. assert not release_deploy.running(tmp_path)
  84. def test_reused_pid_not_signalled(monkeypatch):
  85. monkeypatch.setattr(release_deploy, "start_ticks", lambda pid: "new")
  86. assert not release_deploy.same_process({"pid": 123, "start_ticks": "old"})
  87. assert not release_deploy.same_process({"pid": 1, "start_ticks": "new"})
  88. assert release_deploy.same_process({"pid": 123, "start_ticks": "new"})
  89. @pytest.fixture
  90. def package(source, tmp_path):
  91. archive = release_build.build(source, tmp_path / "dist", "2.0.1", checks=False)
  92. with tarfile.open(archive) as tar:
  93. tar.extractall(tmp_path / "extract", filter="data")
  94. return tmp_path / "extract/vent-agent-2.0.1-linux"
  95. def test_install_preserves_shared_and_activation(package, tmp_path, monkeypatch):
  96. prefix = tmp_path / "installed"
  97. shared = prefix / "shared"
  98. (shared / "data").mkdir(parents=True)
  99. (shared / ".env").write_text("existing-secret")
  100. (shared / "data/sentinel").write_text("keep")
  101. monkeypatch.setattr(release_deploy, "PACKAGE", package)
  102. def fake_run(command, **kwargs):
  103. if "venv" in command:
  104. python = Path(command[-1]) / "bin/python"
  105. python.parent.mkdir(parents=True)
  106. python.write_text("test")
  107. return subprocess.CompletedProcess(command, 0)
  108. monkeypatch.setattr(release_deploy.subprocess, "run", fake_run)
  109. release_deploy.install(prefix)
  110. target = prefix / "releases/2.0.1"
  111. assert (target / ".env").resolve() == shared / ".env"
  112. assert (target / "data").resolve() == shared / "data"
  113. assert (shared / ".env").read_text() == "existing-secret"
  114. assert (shared / "data/sentinel").read_text() == "keep"
  115. assert not (prefix / "current").exists()
  116. with pytest.raises(ValueError, match="不覆盖"):
  117. release_deploy.install(prefix)
  118. release_deploy.activate(prefix, "2.0.1")
  119. assert release_deploy.current_release(prefix) == target
  120. def test_failed_install_cleans_only_new_version(package, tmp_path, monkeypatch):
  121. prefix = tmp_path / "installed"
  122. shared = prefix / "shared"
  123. shared.mkdir(parents=True)
  124. (shared / ".env").write_text("keep")
  125. monkeypatch.setattr(release_deploy, "PACKAGE", package)
  126. def fail(command, **kwargs):
  127. raise subprocess.CalledProcessError(1, command)
  128. monkeypatch.setattr(release_deploy.subprocess, "run", fail)
  129. with pytest.raises(subprocess.CalledProcessError):
  130. release_deploy.install(prefix)
  131. assert not (prefix / "releases/2.0.1").exists()
  132. assert (shared / ".env").read_text() == "keep"