Files
kefu/wechat_rpa/test_database_loading_performance.py
T
2026-09-21 10:34:06 +08:00

176 lines
8.9 KiB
Python

"""Exercise production Qt loader methods without a desktop or real services."""
import ast
import json
import tempfile
import threading
import unittest
from pathlib import Path
from types import MethodType, SimpleNamespace
from unittest import mock
from concurrent.futures import ThreadPoolExecutor
import wxwork_message_browser as browser
import wxwork_local_setup as setup
class LocalFirstLoaderTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
module = ast.parse(Path(__file__).with_name('wechat_gui_qt.py').read_text(encoding='utf-8'))
window = next(item for item in module.body if isinstance(item, ast.ClassDef) and item.name == 'MainWindow')
names = {'_refresh_wechat_database', '_request_wechat_patient_context', '_wechat_database_loaded'}
methods = [item for item in window.body if isinstance(item, ast.FunctionDef) and item.name in names]
for method in methods:
method.decorator_list = []
namespace = {'threading': threading}
exec(compile(ast.Module(body=methods, type_ignores=[]), 'wechat_gui_qt.py', 'exec'), namespace)
cls.methods = {name: namespace[name] for name in names}
def setUp(self):
self.jobs = []
self.events = []
self.host = SimpleNamespace(
_wechat_db_state={}, _wechat_db_selected_account='', _wechat_db_selected_conversation='',
_wechat_db_query='', _wechat_db_load_serial=0, _wechat_db_pending_request=None,
_wechat_patient_loading=False, _wechat_patient_pending=None,
_schedule_console_push=mock.Mock(), _push_console_state=mock.Mock(), append_log=mock.Mock(),
)
for name, method in self.methods.items():
setattr(self.host, name, MethodType(method, self.host))
def emit(result):
self.events.append(dict(result))
self.host._wechat_database_loaded(result)
self.host.wechatDatabaseLoaded = SimpleNamespace(emit=emit)
def thread(**kwargs):
return SimpleNamespace(start=lambda:self.jobs.append((kwargs['name'], kwargs['target'])))
patcher = mock.patch.object(threading, 'Thread', side_effect=thread)
patcher.start();self.addCleanup(patcher.stop)
patcher = mock.patch.object(browser, 'load_browser_snapshot', side_effect=self.snapshot)
self.load = patcher.start();self.addCleanup(patcher.stop)
patcher = mock.patch('backend_client.desktop_patient_context', return_value={'available':True, 'people':[]})
self.patient = patcher.start();self.addCleanup(patcher.stop)
@staticmethod
def snapshot(**kwargs):
return {'loading':False, 'error':'', 'selectedAccount':kwargs['selected_account'] or 'a',
'selectedConversation':kwargs['selected_conversation'] or 'first', 'query':kwargs['query'],
'messages':[{'content':'synthetic local message'}], 'accounts':[]}
def run_job(self, name):
index = next(i for i, item in enumerate(self.jobs) if item[0] == name)
self.jobs.pop(index)[1]()
def test_local_chat_arrives_before_remote_lookup_begins(self):
self.host._refresh_wechat_database()
self.run_job('wechat-database-browser')
self.assertEqual(self.host._wechat_db_state['messages'][0]['content'], 'synthetic local message')
self.assertFalse(self.host._wechat_db_state['loading'])
self.assertTrue(self.host._wechat_db_state['patientContext']['loading'])
self.patient.assert_not_called()
self.run_job('wechat-patient-context')
self.assertTrue(self.host._wechat_db_state['patientContext']['available'])
def test_remote_timeout_keeps_local_chat_visible_and_not_setup_error(self):
self.patient.side_effect = TimeoutError('synthetic timeout')
self.host._refresh_wechat_database();self.run_job('wechat-database-browser')
self.run_job('wechat-patient-context')
state = self.host._wechat_db_state
self.assertFalse(state['loading']);self.assertFalse(state['error'])
self.assertEqual(len(state['messages']), 1)
self.assertIn('synthetic timeout', state['patientContext']['error'])
self.assertFalse(state.get('manualSetupRequired', False))
def test_busy_navigation_is_coalesced_to_latest_account_conversation_and_query(self):
self.host._refresh_wechat_database()
self.host._refresh_wechat_database(selected_account='b',selected_conversation='')
self.host._refresh_wechat_database(query='new',selected_conversation='')
self.host._refresh_wechat_database(selected_conversation='chosen')
self.assertEqual(len(self.jobs),1)
self.run_job('wechat-database-browser');self.run_job('wechat-database-browser')
self.assertEqual(self.load.call_count,2)
self.assertEqual(self.load.call_args.kwargs['selected_account'],'b')
self.assertEqual(self.load.call_args.kwargs['selected_conversation'],'chosen')
self.assertEqual(self.load.call_args.kwargs['query'],'new')
self.assertEqual(len([item for item in self.jobs if item[0]=='wechat-patient-context']),1)
def test_old_patient_result_never_replaces_new_selection_and_only_latest_is_queued(self):
self.host._refresh_wechat_database();self.run_job('wechat-database-browser')
for conversation in ('second','third'):
self.host._refresh_wechat_database(selected_conversation=conversation)
self.run_job('wechat-database-browser')
self.assertEqual(len([item for item in self.jobs if item[0]=='wechat-patient-context']),1)
self.run_job('wechat-patient-context')
self.assertTrue(self.host._wechat_db_state['patientContext']['loading'])
self.assertEqual(self.host._wechat_db_state['selectedConversation'],'third')
self.run_job('wechat-patient-context')
self.assertEqual(self.patient.call_args_list,[mock.call('a','first'),mock.call('a','third')])
def test_repeated_refresh_during_load_does_not_queue_another_initialization(self):
self.host._refresh_wechat_database(refresh_cache=True)
for _ in range(5):
self.host._refresh_wechat_database(refresh_cache=True)
self.run_job('wechat-database-browser')
self.assertEqual(self.load.call_count,1)
self.assertIsNone(self.host._wechat_db_pending_request)
def test_stale_database_result_is_ignored(self):
self.host._wechat_db_load_serial=5
self.host._wechat_database_loaded({'_serial':4,'selectedAccount':'wrong','loading':False})
self.assertEqual(self.host._wechat_db_state,{})
class KeyAcquisitionSingleFlightTest(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
patcher=mock.patch.object(setup,'application_data_dir',return_value=Path(self.temp.name))
patcher.start();self.addCleanup(patcher.stop)
self.assertFalse(setup._key_acquisition_inflight)
def concurrent_run(self, failure=None):
entered=threading.Event();release=threading.Event();waiting=threading.Event()
def once(timeout):
entered.set()
if not release.wait(3):
raise AssertionError('test coordination timed out')
if failure:
raise failure
with mock.patch.object(setup,'_acquire_local_keys_once',side_effect=once) as acquire, ThreadPoolExecutor(2) as pool:
first=pool.submit(setup.acquire_local_keys)
self.assertTrue(entered.wait(2))
future=next(iter(setup._key_acquisition_inflight.values()))
original=future.result
def joined(*args,**kwargs):
waiting.set();return original(*args,**kwargs)
with mock.patch.object(future,'result',side_effect=joined):
second=pool.submit(setup.acquire_local_keys)
try:
self.assertTrue(waiting.wait(2))
finally:
release.set()
if failure:
for result in (first,second):
with self.assertRaisesRegex(ValueError,'synthetic'):
result.result(2)
else:
first.result(2);second.result(2)
self.assertEqual(acquire.call_count,1)
self.assertFalse(setup._key_acquisition_inflight)
def test_simultaneous_browser_and_protocol_initialization_start_one_worker(self):
self.concurrent_run()
def test_failure_is_shared_and_does_not_poison_later_retry(self):
self.concurrent_run(ValueError('synthetic failure'))
with mock.patch.object(setup,'_acquire_local_keys_once') as acquire:
setup.acquire_local_keys()
acquire.assert_called_once()
def test_completed_result_is_not_reused_for_later_explicit_refresh(self):
with mock.patch.object(setup,'_acquire_local_keys_once') as acquire:
setup.acquire_local_keys();setup.acquire_local_keys()
self.assertEqual(acquire.call_count,2)
if __name__=='__main__':
unittest.main()