"""pr.py 子进程超时单测。 真实路径:sleep 命令实测超时真的杀进程、真的按时返回 (不是等命令跑完);环境变量改超时、非法值回落默认。 映射路径:假 runner 回 124 超时结果时,push/commit 按原有 失败路径抛 PrStepError(step 对得上),不挂住、不误报成功。 """ import os import sys import tempfile import time import unittest from pathlib import Path from types import SimpleNamespace try: from cloud.pr import ( PrStepError, _pr_subprocess_timeout, _run, create_task_pr, ) except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.pr import ( PrStepError, _pr_subprocess_timeout, _run, create_task_pr, ) def _result(returncode=0, stdout="", stderr=""): return SimpleNamespace(returncode=returncode, stdout=stdout, stderr=stderr) class _Git: """假 git:push/commit 的失败模式可配,其余按脚本回。""" def __init__(self, push="ok", commit="nothing"): self.push = push self.commit = commit def __call__(self, cmd, cwd): if "remote" in cmd: return _result(0, "https://github.com/octo/a.git\n") if "branch" in cmd: return _result(0, "host/t1\n") if "add" in cmd: return _result(0, "") if "commit" in cmd: if self.commit == "timeout": return _result(124, "", "command timed out") return _result(1, "", "nothing to commit, working tree clean") if "push" in cmd: if self.push == "timeout": return _result(124, "", "command timed out") return _result(0, "") return _result(0, "") def _gh(cmd, cwd): if cmd[:2] == ["gh", "auth"]: return _result(0, "", "") if cmd[:2] == ["gh", "pr"]: return _result(0, "https://github.com/octo/a/pull/9\n") return _result(0, "") class RunTimeoutTestCase(unittest.TestCase): def test_real_sleep_is_killed_on_timeout(self): start = time.monotonic() r = _run(["sleep", "3"], timeout=1) elapsed = time.monotonic() - start self.assertEqual(r.returncode, 124) self.assertIn("timed out", r.stderr) self.assertLess(elapsed, 2.5) def test_fast_command_ok_with_default_timeout(self): r = _run(["true"]) self.assertEqual(r.returncode, 0) def test_env_override_and_invalid_fallback(self): old = os.environ.get("CLOUD_PR_SUBPROCESS_TIMEOUT") try: os.environ["CLOUD_PR_SUBPROCESS_TIMEOUT"] = "1" self.assertEqual(_pr_subprocess_timeout(), 1) start = time.monotonic() r = _run(["sleep", "3"]) self.assertEqual(r.returncode, 124) self.assertLess(time.monotonic() - start, 2.5) os.environ["CLOUD_PR_SUBPROCESS_TIMEOUT"] = "abc" self.assertEqual(_pr_subprocess_timeout(), 120) os.environ["CLOUD_PR_SUBPROCESS_TIMEOUT"] = "0" self.assertEqual(_pr_subprocess_timeout(), 120) finally: if old is None: os.environ.pop("CLOUD_PR_SUBPROCESS_TIMEOUT", None) else: os.environ["CLOUD_PR_SUBPROCESS_TIMEOUT"] = old class TimeoutMappingTestCase(unittest.TestCase): def _worktree(self, tmp): d = os.path.join(tmp, "t1") os.makedirs(d, exist_ok=True) return d def test_push_timeout_is_pr_step_error(self): with tempfile.TemporaryDirectory() as tmp: self._worktree(tmp) with self.assertRaises(PrStepError) as ctx: create_task_pr("t1", 1, worktrees_root=tmp, git_runner=_Git(push="timeout"), gh_runner=_gh, gh_which="/usr/bin/gh") self.assertEqual(ctx.exception.step, "push") self.assertIn("timed out", ctx.exception.message) def test_commit_timeout_is_pr_step_error(self): with tempfile.TemporaryDirectory() as tmp: self._worktree(tmp) with self.assertRaises(PrStepError) as ctx: create_task_pr("t1", 1, worktrees_root=tmp, git_runner=_Git(commit="timeout"), gh_runner=_gh, gh_which="/usr/bin/gh") self.assertEqual(ctx.exception.step, "commit") if __name__ == "__main__": unittest.main()