Take main's svc recover test with the shared free-port helper
ci / svc-realcombo (pull_request) Successful in 3m13s
ci / test (pull_request) Successful in 9m50s
ci / test-pg (pull_request) Successful in 15m28s

Why: the initiative branch's merge resolution kept a stale test_svc_recover.py that lost main's free_fixture_port helper while its callers (test_svc_superadmin_envfile) reference it, breaking the CI test job on every initiative-branch head.
Evidence: main @33d6ef69 has the helper and a fully green CI (test/test-pg/svc-realcombo success); this branch's callers now match.
Impact: Test helper alignment only; no production code.
Refs: initiative PR #635; merge af2e6b41
This commit is contained in:
2026-10-22 06:44:06 +00:00
parent 89a6054941
commit e8de6ba97b
+18 -28
View File
@@ -184,12 +184,17 @@ esac
"""
def free_fixture_port():
with socket.socket() as reservation:
reservation.bind(("127.0.0.1", 0))
return reservation.getsockname()[1]
class SvcAutoRecoverTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.mkdtemp(prefix="svc-autorecover-")
self.addCleanup(shutil.rmtree, tmp, True)
self.root = Path(tmp)
self._allocated_ports = set()
(self.root / "scripts").mkdir(parents=True)
shutil.copy2(SVC_SCRIPT, self.root / "scripts" / "svc.sh")
self.trace = self.root / "trace"
@@ -249,21 +254,6 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(proc.returncode, 0, proc.stdout)
return proc
def available_port(self):
"""Allocate a distinct service port for this test, including address changes.
The socket closes before svc.sh binds it, leaving a small allocation-to-start
race with unrelated processes. Keep allocated numbers distinct within the
test so stopped/restarted services never share another instance's port.
"""
while True:
with socket.socket() as sock:
sock.bind(("0.0.0.0", 0))
port = sock.getsockname()[1]
if port not in self._allocated_ports:
self._allocated_ports.add(port)
return port
def runtime(self, name):
return self.root / "runtime" / name
@@ -346,14 +336,14 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 1. 期望态:start 成功置位 / 显式 stop 清除(含进程已死的 stop)----
def test_start_records_autostart_and_stop_clears(self):
self.seed_agent("dev", self.available_port())
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.assertEqual(self.kv("dev", "autostart"), "1")
self.svc("dev", "stop")
self.assertIsNone(self.kv("dev", "autostart"))
def test_stop_on_dead_process_clears_autostart(self):
self.seed_agent("dev", self.available_port())
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.kill9("dev")
# 进程已死但 autostart 保留(等待 recover);显式 stop 表示"保持停止"。
@@ -364,7 +354,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 2. recover:崩溃拉回 / 运行中不动 / 显式停止不复活 / 未启动不碰 ----
def test_recover_restarts_sigkilled_service(self):
port = self.available_port()
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
old_pid = self.kill9("dev")
@@ -381,7 +371,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertIn("--addr 127.0.0.1:%d" % port, self.kv("dev", "exec"))
def test_recover_noop_while_running(self):
self.seed_agent("dev", self.available_port())
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
pid = self.pid_of("dev")
result = self.svc("recover")
@@ -390,7 +380,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_respects_explicit_stop(self):
self.seed_agent("dev", self.available_port())
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.svc("dev", "stop")
result = self.svc("recover")
@@ -399,7 +389,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertEqual(self.history_events("dev", "recover"), [])
def test_recover_ignores_never_started_service(self):
self.seed_agent("dev", self.available_port()) # 从未 start:无 autostart 键
self.seed_agent("dev", free_fixture_port()) # 从未 start:无 autostart 键
result = self.svc("recover")
self.assertEqual(result.returncode, 0, result.stdout)
self.assertFalse(self.pid_alive(self.pid_of("dev")))
@@ -408,7 +398,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 3. 并发防护:生命周期锁被持有时 recover 跳过 ----
def test_recover_skips_when_lock_held(self):
self.seed_agent("dev", self.available_port())
self.seed_agent("dev", free_fixture_port())
self.start_ok("dev")
self.kill9("dev")
lock = self.runtime("dev") / ".start.lock"
@@ -428,7 +418,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
# ---- 4. 恢复失败路径:端口被外部进程占用 → fail 事件 + 非零退出 ----
def test_recover_fails_when_port_stolen(self):
port = self.available_port()
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
self.kill9("dev")
@@ -453,7 +443,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
if bound:
break
time.sleep(0.05)
self.assertTrue(bound, f"thief 未能在 5s 内监听 {port}")
self.assertTrue(bound, "thief 未能在 5s 内监听 18808")
try:
result = self.svc("recover", check=False)
self.assertEqual(result.returncode, 1, result.stdout)
@@ -497,7 +487,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
"""模拟 update 误清 pidfile 后本 bin 仍占口:裸 start 应回收并拉起新进程。"""
if not self._ss_can_attribute_pid():
self.skipTest("ss -ltnp 无法归因 PID(reclaim_own_port 需此能力)")
port = self.available_port()
port = free_fixture_port()
self.seed_agent("dev", port)
self.start_ok("dev")
old = self.pid_of("dev")
@@ -518,7 +508,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
def test_start_rejects_foreign_port_occupant(self):
"""外部 python http.server 占口:start 必须失败并提示非本服务。"""
port = self.available_port()
port = free_fixture_port()
self.seed_agent("dev", port)
thief = subprocess.Popen(
["python3", "-m", "http.server", str(port), "--bind", "127.0.0.1"],
@@ -555,7 +545,7 @@ class SvcAutoRecoverTests(unittest.TestCase):
self.assertFalse((unit_dir / "agent-system-svc-recover.service").exists())
def test_recover_reserved_name_rejected(self):
self.seed_agent("recover", self.available_port()) # 保留字不可作服务名
self.seed_agent("recover", free_fixture_port()) # 保留字不可作服务名
result = self.svc("recover", "recover", check=False)
self.assertEqual(result.returncode, 2, result.stdout)