From e8de6ba97bc0d272af561269a10be5ec246431df Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 22 Oct 2026 06:44:06 +0000 Subject: [PATCH] Take main's svc recover test with the shared free-port helper 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 --- tests/test_svc_recover.py | 46 +++++++++++++++------------------------ 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/tests/test_svc_recover.py b/tests/test_svc_recover.py index b02352f1..5030c83e 100644 --- a/tests/test_svc_recover.py +++ b/tests/test_svc_recover.py @@ -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)