Files
.pouch/tests/test_deployer_sync.py

155 lines
5.8 KiB
Python

from __future__ import annotations
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
DEPLOY_SCRIPTS = REPO_ROOT / "skills" / "deployer" / "scripts" / "deploy"
SYNC_PY = DEPLOY_SCRIPTS / "sync.py"
if str(DEPLOY_SCRIPTS) not in sys.path:
sys.path.insert(0, str(DEPLOY_SCRIPTS))
sync_spec = importlib.util.spec_from_file_location("deployer_sync", SYNC_PY)
assert sync_spec is not None and sync_spec.loader is not None
deployer_sync = importlib.util.module_from_spec(sync_spec)
sync_spec.loader.exec_module(deployer_sync)
def _info(service_dir: str, **overrides) -> dict:
info = {
"service_dir": service_dir,
"name": "demo-service",
"node": "edge",
"remote_dir": "/opt/app/svc",
"sync_exclude": ["data", "_data"],
"port": None,
"identity_file": None,
}
info.update(overrides)
return info
class DeployerSyncTests(unittest.TestCase):
def test_collect_sync_files_skips_excluded_directories(self) -> None:
with tempfile.TemporaryDirectory() as temp:
src = Path(temp)
(src / "compose.yaml").write_text("ok\n", encoding="utf-8")
(src / "data").mkdir()
(src / "data" / "db").write_text("nope\n", encoding="utf-8")
nested = src / "cfg"
nested.mkdir()
(nested / "app.env").write_text("x\n", encoding="utf-8")
(nested / "data").mkdir()
(nested / "data" / "cache").write_text("nope\n", encoding="utf-8")
files = {
p.relative_to(src).as_posix()
for p in deployer_sync.collect_sync_files(str(src), ["data", "_data"])
}
self.assertEqual(files, {"compose.yaml", "cfg/app.env"})
def test_prefers_rsync_when_available(self) -> None:
info = _info("/tmp/svc")
completed = subprocess.CompletedProcess(["rsync"], 0)
with mock.patch.object(deployer_sync.shutil, "which", return_value="/usr/bin/rsync"):
with mock.patch.object(
deployer_sync.subprocess, "run", return_value=completed
) as run:
rc = deployer_sync.sync_directory(info)
self.assertEqual(rc, 0)
cmd = run.call_args[0][0]
self.assertEqual(cmd[0], "rsync")
self.assertIn("--delete", cmd)
self.assertIn("--exclude=data/", cmd)
def test_falls_back_to_scp_when_rsync_missing(self) -> None:
with tempfile.TemporaryDirectory() as temp:
src = Path(temp)
(src / "compose.yaml").write_text("ok\n", encoding="utf-8")
(src / "data").mkdir()
(src / "data" / "db").write_text("nope\n", encoding="utf-8")
nested = src / "cfg"
nested.mkdir()
(nested / "app.env").write_text("x\n", encoding="utf-8")
commands: list[list[str]] = []
def fake_run(cmd, **kwargs):
commands.append(list(cmd))
return subprocess.CompletedProcess(cmd, 0)
info = _info(str(src), port=2222, identity_file="/tmp/key")
with mock.patch.object(deployer_sync.shutil, "which", return_value=None):
with mock.patch.object(
deployer_sync.subprocess, "run", side_effect=fake_run
):
rc = deployer_sync.sync_directory(info)
self.assertEqual(rc, 0)
self.assertTrue(commands)
mkdir_cmd = next(
command
for command in commands
if command and command[0] == "ssh" and command[-1].startswith("mkdir -p ")
)
self.assertEqual(mkdir_cmd[0], "ssh")
self.assertIn("-p", mkdir_cmd)
self.assertIn("2222", mkdir_cmd)
self.assertIn("/opt/app/svc/cfg", " ".join(mkdir_cmd))
scp_cmds = [c for c in commands if c and c[0] == "scp"]
self.assertEqual(len(scp_cmds), 2)
joined = "\n".join(" ".join(c) for c in scp_cmds)
self.assertIn("compose.yaml", joined)
self.assertIn("app.env", joined)
self.assertNotIn("db", joined)
self.assertNotIn("--delete", joined)
self.assertIn("-P", scp_cmds[0])
self.assertIn("2222", scp_cmds[0])
self.assertIn("-i", scp_cmds[0])
self.assertIn("/tmp/key", scp_cmds[0])
def test_rsync_filenotfound_falls_back_to_scp(self) -> None:
with tempfile.TemporaryDirectory() as temp:
src = Path(temp)
(src / "compose.yaml").write_text("ok\n", encoding="utf-8")
commands: list[list[str]] = []
def fake_run(cmd, **kwargs):
if cmd and cmd[0] == "rsync":
raise FileNotFoundError("rsync")
commands.append(list(cmd))
return subprocess.CompletedProcess(cmd, 0)
with mock.patch.object(
deployer_sync.shutil, "which", return_value="/usr/bin/rsync"
):
with mock.patch.object(
deployer_sync.subprocess, "run", side_effect=fake_run
):
rc = deployer_sync.sync_directory(_info(str(src)))
self.assertEqual(rc, 0)
self.assertTrue(any(c and c[0] == "scp" for c in commands))
def test_docs_say_scp_fallback_not_tar(self) -> None:
compose = (
REPO_ROOT / "skills" / "deployer" / "references" / "compose.md"
).read_text(encoding="utf-8")
check = (DEPLOY_SCRIPTS / "check.py").read_text(encoding="utf-8")
self.assertIn("本机无 rsync 时 scp 兜底", compose)
self.assertNotIn("本机无 rsync 时 tar over SSH 兜底", compose)
self.assertIn("scp fallback", check)
self.assertNotIn("tar-over-SSH", check)
if __name__ == "__main__":
unittest.main()