# -*- 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()