feat(rpa): ensure_profile_free for same-profile browser release (1.2.13)
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 25s
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 25s
Add precise Chrome/Edge quit by user-data-dir and stale SingletonLock cleanup so publish RPAs can free account-manager windows before launch without killing the default browser.
This commit is contained in:
134
tests/test_profile_lock.py
Normal file
134
tests/test_profile_lock.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Profile 精准关闭 / 僵锁清理单元测试。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from jiangchang_skill_core.rpa.errors import ERR_PROFILE_IN_USE
|
||||
from jiangchang_skill_core.rpa.profile_lock import (
|
||||
clear_stale_singleton_locks,
|
||||
ensure_profile_free,
|
||||
force_quit_profile_browsers,
|
||||
is_profile_lock_error,
|
||||
)
|
||||
|
||||
|
||||
class TestProfileLockHelpers(unittest.TestCase):
|
||||
def test_is_profile_lock_error(self) -> None:
|
||||
self.assertTrue(
|
||||
is_profile_lock_error(
|
||||
RuntimeError("User data directory is already in use")
|
||||
)
|
||||
)
|
||||
self.assertTrue(is_profile_lock_error(RuntimeError("SingletonLock")))
|
||||
self.assertFalse(is_profile_lock_error(RuntimeError("timeout waiting")))
|
||||
|
||||
def test_force_quit_rejects_short_path(self) -> None:
|
||||
self.assertEqual(force_quit_profile_browsers("C:\\a"), 0)
|
||||
|
||||
def test_clear_stale_singleton_when_no_pids(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
profile = Path(tmp) / "account-manager" / "profiles" / "acc-1"
|
||||
profile.mkdir(parents=True)
|
||||
lock = profile / "SingletonLock"
|
||||
cookie = profile / "SingletonCookie"
|
||||
lock.write_text("deadhost-999999", encoding="utf-8")
|
||||
cookie.write_text("x", encoding="utf-8")
|
||||
with mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.list_profile_browser_pids",
|
||||
return_value=[],
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock._pid_alive",
|
||||
return_value=False,
|
||||
):
|
||||
removed = clear_stale_singleton_locks(str(profile))
|
||||
self.assertTrue(removed)
|
||||
self.assertFalse(lock.exists())
|
||||
self.assertFalse(cookie.exists())
|
||||
|
||||
def test_clear_stale_skips_when_pids_present(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
profile = Path(tmp) / "profiles" / "acc-2"
|
||||
profile.mkdir(parents=True)
|
||||
lock = profile / "SingletonLock"
|
||||
lock.write_text("host-1", encoding="utf-8")
|
||||
with mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.list_profile_browser_pids",
|
||||
return_value=[1234],
|
||||
):
|
||||
removed = clear_stale_singleton_locks(str(profile))
|
||||
self.assertFalse(removed)
|
||||
self.assertTrue(lock.exists())
|
||||
|
||||
def test_ensure_profile_free_quits_then_ok(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
profile = Path(tmp) / "profiles" / "acc-long-enough"
|
||||
profile.mkdir(parents=True)
|
||||
with mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.list_profile_browser_pids",
|
||||
side_effect=[[111], []],
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.force_quit_profile_browsers",
|
||||
return_value=1,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.clear_stale_singleton_locks",
|
||||
return_value=False,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.time.sleep",
|
||||
):
|
||||
result = ensure_profile_free(
|
||||
str(profile), wait_sec=0, raise_if_busy=True
|
||||
)
|
||||
self.assertTrue(result.freed)
|
||||
self.assertEqual(result.pids_before, (111,))
|
||||
self.assertEqual(result.pids_after, ())
|
||||
self.assertEqual(result.killed_ops, 1)
|
||||
|
||||
def test_ensure_profile_free_raises_when_still_busy(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
profile = Path(tmp) / "profiles" / "acc-still-busy"
|
||||
profile.mkdir(parents=True)
|
||||
with mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.list_profile_browser_pids",
|
||||
return_value=[222],
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.force_quit_profile_browsers",
|
||||
return_value=1,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.clear_stale_singleton_locks",
|
||||
return_value=False,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.time.sleep",
|
||||
):
|
||||
with self.assertRaises(RuntimeError) as ctx:
|
||||
ensure_profile_free(str(profile), wait_sec=0, raise_if_busy=True)
|
||||
self.assertIn(ERR_PROFILE_IN_USE, str(ctx.exception))
|
||||
|
||||
def test_ensure_profile_free_no_raise_when_requested(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
profile = Path(tmp) / "profiles" / "acc-keep-busy"
|
||||
profile.mkdir(parents=True)
|
||||
with mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.list_profile_browser_pids",
|
||||
return_value=[333],
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.force_quit_profile_browsers",
|
||||
return_value=0,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.clear_stale_singleton_locks",
|
||||
return_value=False,
|
||||
), mock.patch(
|
||||
"jiangchang_skill_core.rpa.profile_lock.time.sleep",
|
||||
):
|
||||
result = ensure_profile_free(
|
||||
str(profile), wait_sec=0, raise_if_busy=False
|
||||
)
|
||||
self.assertFalse(result.freed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user