Files
host f73c3f7a05 pr 超时:git/gh 子进程默认 120 秒超时,超时转 502 不挂 handler
此前 /pr 的子进程调用没有超时:远端通路卡死时 handler
线程永久挂住,客户端断开也收不回来。_run 真实路径一律
带 timeout(默认 120 秒,CLOUD_PR_SUBPROCESS_TIMEOUT
可改,非法值回落默认),超时杀子进程、返回 124,调用方
按原有失败路径处理(push/pr_create/commit 抛 PrStepError,
探针按跳过处理)。App 路径的 GitHub API 调用已有 30 秒
超时,本次不动。测试:真实 sleep 超时 2 个、环境变量
2 项、超时映射 push/commit 2 个
2026-10-26 17:01:47 +08:00

134 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()