"""deploy.py 门禁编排单测(CI 第二阶段覆盖率补点)。 只测编排层:run() 的 check 开关、check_clean_tree 的 拒绝/告警/放行、deploy_via_git 的 dry-run 与各门禁拒绝、 main() 的 git 分发与 tar 路径前置检查。真实 SSH/传输仍由 test_deploy.py 的 transfer/remote_sha 测试覆盖,本文件不 碰网络与主机。 """ import os import subprocess import sys import unittest from types import SimpleNamespace from unittest import mock REPO_ROOT = os.path.join(os.path.dirname(__file__), "..", "..") sys.path.insert(0, os.path.join(REPO_ROOT, "deploy")) import deploy as deploy_mod # noqa: E402 def _args(**over): base = dict(tarball=None, build_only=False, host=None, user="root", dry_run=True, repo="git@github.com:openqj/codex-app-server", git_key="/tmp/k", allow_dirty=False, skip_tests=False, ref="HEAD", commit=None, source="git", out_dir="/tmp/out") base.update(over) return SimpleNamespace(**base) class RunAndCleanTreeTest(unittest.TestCase): def test_run_check_exits_on_failure(self): bad = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="boom") with mock.patch.object(deploy_mod.subprocess, "run", return_value=bad): with self.assertRaises(SystemExit) as ctx: deploy_mod.run(["false"]) self.assertEqual(ctx.exception.code, 1) def test_run_no_check_returns_result(self): bad = subprocess.CompletedProcess(args=[], returncode=1, stdout="o", stderr="e") with mock.patch.object(deploy_mod.subprocess, "run", return_value=bad): self.assertIs(deploy_mod.run(["false"], check=False), bad) def test_clean_tree_dirty_refuses_and_warns(self): dirty = SimpleNamespace(returncode=1) with mock.patch.object(deploy_mod, "run", return_value=dirty): with self.assertRaises(SystemExit) as ctx: deploy_mod.check_clean_tree(False) self.assertEqual(ctx.exception.code, 2) deploy_mod.check_clean_tree(True) # 只告警,不退出 def test_clean_tree_clean_passes(self): clean = SimpleNamespace(returncode=0) with mock.patch.object(deploy_mod, "run", return_value=clean): deploy_mod.check_clean_tree(False) class DeployViaGitTest(unittest.TestCase): def _patched(self, commit="a" * 40, on_main=True): return (mock.patch.object(deploy_mod, "check_clean_tree"), mock.patch.object(deploy_mod, "run_tests"), mock.patch.object(deploy_mod, "git_commit", return_value=commit), mock.patch.object(deploy_mod, "commit_on_origin_main", return_value=on_main)) def test_dry_run_success(self): patches = self._patched() for p in patches: p.start() try: self.assertEqual(deploy_mod.deploy_via_git(_args()), 0) finally: for p in patches: p.stop() def test_gates(self): cases = [ (_args(tarball="/tmp/x.tgz"), "tarball 冲突"), (_args(repo="https://github.com/o/r"), "非 SSH 仓库"), ] for args, _why in cases: patches = self._patched() for p in patches: p.start() try: with self.assertRaises(SystemExit) as ctx: deploy_mod.deploy_via_git(args) self.assertEqual(ctx.exception.code, 2) finally: for p in patches: p.stop() patches = self._patched(commit="abc") for p in patches: p.start() try: with self.assertRaises(SystemExit) as ctx: deploy_mod.deploy_via_git(_args()) self.assertEqual(ctx.exception.code, 2) finally: for p in patches: p.stop() patches = self._patched(on_main=False) for p in patches: p.start() try: with self.assertRaises(SystemExit) as ctx: deploy_mod.deploy_via_git(_args()) self.assertEqual(ctx.exception.code, 2) finally: for p in patches: p.stop() class MainDispatchTest(unittest.TestCase): def test_main_dispatches_git(self): seen = [] with mock.patch.object( deploy_mod, "deploy_via_git", side_effect=lambda a: seen.append(a) or 0): rc = deploy_mod.main(["h", "root", "--source", "git", "--dry-run"]) self.assertEqual(rc, 0) self.assertEqual(seen[0].source, "git") self.assertTrue(seen[0].dry_run) def test_main_tar_needs_host_or_mode(self): with self.assertRaises(SystemExit) as ctx: deploy_mod.main([]) self.assertEqual(ctx.exception.code, 2) def test_main_build_only_tar_path(self): with mock.patch.object(deploy_mod, "check_clean_tree"), \ mock.patch.object(deploy_mod, "run_tests"), \ mock.patch.object( deploy_mod, "build_release", return_value=("tb.tgz", "sha", "c" * 40)): rc = deploy_mod.main(["myhost", "--build-only"]) self.assertEqual(rc, 0) if __name__ == "__main__": unittest.main()