"""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()