2026-10-29 14:39:39 +03:00 | | | '''Проверки создания и авторизации без запуска реальных терминалов.'''
|
| | |
|
| | | import copy
|
| | | import json
|
| | | import os
|
2026-10-31 06:48:02 +03:00 | | | import shutil
|
2026-10-29 14:39:39 +03:00 | | | import tempfile
|
| | | import threading
|
| | | import time
|
| | | import unittest
|
| | | from pathlib import Path
|
| | | from unittest.mock import patch
|
| | |
|
| | | from config import ConfigStore
|
2026-10-31 07:26:44 +03:00 | | | from mt5_control import MT5_Control
|
2026-10-29 14:39:39 +03:00 | | | from terminal_manager import SERVICE_REL, TerminalManager, account_values, validate_folder
|
| | |
|
| | |
|
| | | class SimulatedManager(TerminalManager):
|
| | | '''Заменяет только границу с процессом и сервисом терминала.'''
|
| | |
|
| | | def __init__(self, store):
|
| | | super().__init__(store)
|
| | | self.running = set()
|
| | | self.saved_accounts = {}
|
| | | self.current_accounts = {}
|
| | | self.launches = []
|
| | | self.fail_wait = False
|
| | |
|
| | | def processes(self, folder):
|
| | | return [object()] if folder in self.running else []
|
| | |
|
| | | def launch(self, folder, config=None):
|
| | | self.running.add(folder)
|
| | | self.launches.append((folder, config is not None))
|
| | | if config:
|
| | | values = dict(line.split('=', 1) for line in config.read_text(encoding='utf-16').splitlines() if '=' in line)
|
| | | self.current_accounts[folder] = (int(values['Login']), values['Server'], values['Password'])
|
| | | else:
|
| | | self.current_accounts[folder] = self.saved_accounts.get(folder)
|
| | | # Без файла /config пароль не должен оставаться в папке запуска.
|
| | | directory = Path(self.control.instance_path(folder)).parent
|
| | | assert not list(directory.glob('manager-login-*.ini'))
|
| | |
|
| | | def stop(self, folder, *, graceful=False):
|
| | | if graceful and folder in self.current_accounts:
|
| | | self.saved_accounts[folder] = self.current_accounts[folder]
|
| | | self.running.discard(folder)
|
| | |
|
| | | def wait_state(self, folder, since, account=None):
|
| | | if self.fail_wait:
|
| | | raise RuntimeError('Имитирована ошибка запуска.')
|
| | | current = self.current_accounts.get(folder)
|
| | | if account and current != account:
|
| | | raise RuntimeError('Счёт не совпал.')
|
| | | return {'connected': bool(current), 'login': current[0] if current else 0,
|
| | | 'server': current[1] if current else ''}
|
| | |
|
| | |
|
| | | class ProvisioningTests(unittest.TestCase):
|
| | | def setUp(self):
|
| | | self.temporary = tempfile.TemporaryDirectory()
|
| | | self.root = Path(self.temporary.name)
|
| | | self.template = self.root / 'template'
|
| | | (self.template / 'config').mkdir(parents=True)
|
| | | for filename in ('terminal64.exe', 'MetaEditor64.exe', 'ServiceStateWriter.ex5'):
|
| | | (self.template / filename).write_bytes(b'MZ-test-binary')
|
| | | (self.template / 'config/servers.dat').write_bytes(b'test-server-directory')
|
| | | self.path = self.root / 'config.json'
|
| | | self.path.write_text(json.dumps({'terminals': {}, 'mt5_folder': str(self.root / 'instances'),
|
| | | 'mt5_exe': 'terminal64.exe', 'database': str(self.root / 'custom.sqlite'),
|
| | | 'terminal_template': str(self.template)}), encoding='utf-8')
|
| | | self.manager = SimulatedManager(ConfigStore(self.path))
|
| | |
|
| | | def tearDown(self):
|
| | | self.manager.close()
|
| | | self.temporary.cleanup()
|
| | |
|
| | | def run_job(self, action, name, record=None):
|
| | | task = self.manager.submit(action, name, record)
|
| | | deadline = time.monotonic() + 5
|
| | | while time.monotonic() < deadline:
|
| | | task = self.manager.job(task['id'])
|
| | | if task['status'] in ('done', 'error'):
|
| | | return task
|
| | | time.sleep(0.01)
|
| | | self.fail('Фоновая операция не завершилась.')
|
| | |
|
2026-10-31 06:48:02 +03:00 | | | @staticmethod
|
| | | def prepare_project(project):
|
| | | (project / 'static').mkdir(parents=True, exist_ok=True)
|
| | | shutil.copyfile(Path(__file__).resolve().parent.parent / 'static/services.ini',
|
| | | project / 'static/services.ini')
|
| | |
|
2026-10-29 14:39:39 +03:00 | | | def test_create_with_account_then_plain_restart(self):
|
| | | task = self.run_job('create', 'MetaTrader5.1', {'login': 123, 'server': 'Broker-Demo', 'password': 'secret'})
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | self.assertEqual(self.manager.launches, [('MetaTrader5.1', True), ('MetaTrader5.1', False)])
|
| | | record = self.manager.store.data['terminals']['MetaTrader5.1']
|
2026-10-31 07:26:44 +03:00 | | | target = Path(self.manager.control.instance_path('MetaTrader5.1')).parent
|
| | | self.assertNotIn('path', record)
|
| | | self.assertNotIn('path', json.loads(self.path.read_text())['terminals']['MetaTrader5.1'])
|
2026-10-29 14:39:39 +03:00 | | | self.assertTrue((target / 'config/servers.dat').is_file())
|
| | | self.assertTrue((target / 'MQL5/Services/Shared Projects/mt5-manager/ServiceStateWriter.ex5').is_file())
|
| | | self.assertIn('InpDatabase=custom.sqlite', (target / 'config/services.ini').read_text(encoding='utf-16'))
|
| | | self.assertFalse(list(target.glob('manager-login-*.ini')))
|
| | | self.assertNotIn('secret', json.dumps(self.manager.settings()))
|
| | | self.assertNotIn('secret', json.dumps(self.manager.snapshot()))
|
| | | self.manager.stop('MetaTrader5.1')
|
| | | self.assertEqual(self.run_job('start', 'MetaTrader5.1')['status'], 'done')
|
| | | self.assertFalse(self.manager.launches[-1][1])
|
| | |
|
| | | def test_create_without_account(self):
|
| | | task = self.run_job('create', 'Blank')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | self.assertEqual(self.manager.launches, [('Blank', False)])
|
| | | record = self.manager.store.data['terminals']['Blank']
|
| | | self.assertEqual((record['login'], record['server'], record['password']), ('', '', ''))
|
| | |
|
| | | def test_project_operation_blocks_terminal_commands_until_finished(self):
|
| | | entered, release = threading.Event(), threading.Event()
|
| | |
|
| | | def check():
|
| | | entered.set()
|
| | | release.wait(5)
|
| | | return {'behind': 0}
|
| | |
|
| | | with patch.object(self.manager.updates, 'check', side_effect=check):
|
| | | self.manager.submit('check_updates', '')
|
| | | self.assertTrue(entered.wait(2))
|
| | | try:
|
| | | with self.assertRaises(ValueError):
|
| | | self.manager.submit('create', 'Blocked')
|
| | | with self.assertRaises(ValueError):
|
| | | self.manager.submit('deploy_service', '')
|
| | | self.assertEqual(self.manager.snapshot(), {})
|
| | | finally:
|
| | | release.set()
|
| | |
|
| | | def test_project_operation_waits_for_terminal_jobs(self):
|
| | | entered, release = threading.Event(), threading.Event()
|
| | |
|
| | | def wait_state(*args, **kwargs):
|
| | | entered.set()
|
| | | release.wait(5)
|
| | | return {}
|
| | |
|
| | | with patch.object(self.manager, 'wait_state', side_effect=wait_state):
|
| | | self.manager.submit('create', 'Pending')
|
| | | self.assertTrue(entered.wait(2))
|
| | | try:
|
| | | with self.assertRaises(ValueError):
|
| | | self.manager.submit('check_updates', '')
|
| | | finally:
|
| | | release.set()
|
| | |
|
2026-10-31 07:14:06 +03:00 | | | def test_deploy_job_exposes_copy_failure_details_without_account_password(self):
|
| | | result = {'copied': [], 'failed': ['One'], 'restart_required': [],
|
| | | 'errors': {'One': 'Access is denied: C:/instances/One/service.ex5'}}
|
| | | with patch.object(self.manager.updates, 'deploy', return_value=result):
|
| | | task = self.run_job('deploy_service', '')
|
| | | self.assertEqual(task['status'], 'error')
|
| | | self.assertIn('Access is denied', task['error'])
|
| | | self.assertIn('C:/instances/One/service.ex5', task['error'])
|
| | | self.assertEqual(task['result']['errors'], result['errors'])
|
| | |
|
2026-10-29 14:39:39 +03:00 | | | def test_missing_uploads_use_reference_terminal_and_compile_service(self):
|
| | | reference = self.root / 'reference'
|
| | | (reference / 'config').mkdir(parents=True)
|
| | | for filename in ('terminal64.exe', 'MetaEditor64.exe'):
|
| | | (reference / filename).write_bytes(b'MZ-reference-' + filename.encode())
|
| | | (self.template / filename).unlink()
|
| | | (reference / 'config/servers.dat').write_bytes(b'reference-servers')
|
| | | (self.template / 'ServiceStateWriter.ex5').unlink()
|
| | |
|
2026-10-31 06:48:02 +03:00 | | | project = reference / 'MQL5/Shared Projects/mt5-manager'
|
| | | self.prepare_project(project)
|
| | |
|
| | | def compile_service(editor, source, output):
|
2026-10-29 14:39:39 +03:00 | | | output.parent.mkdir(parents=True, exist_ok=True)
|
| | | output.write_bytes(b'compiled-service')
|
| | | return True
|
| | |
|
| | | with patch.object(self.manager, 'reference_terminal', return_value=reference), \
|
2026-10-31 06:48:02 +03:00 | | | patch('terminal_manager.PROJECT_DIR', project), \
|
| | | patch('terminal_manager.compile_service', side_effect=compile_service) as compile_call:
|
2026-10-29 14:39:39 +03:00 | | | # Просмотр настроек только показывает доступные файлы.
|
| | | self.assertTrue(self.manager.settings()['files']['terminal']['exists'])
|
| | | compile_call.assert_not_called()
|
| | | task = self.run_job('create', 'Fallback')
|
| | | self.assertEqual(task['status'], 'done', task)
|
2026-10-31 06:48:02 +03:00 | | | compile_call.assert_called_once_with(reference / 'MetaEditor64.exe',
|
| | | project / 'ServiceStateWriter.mq5', reference / SERVICE_REL)
|
2026-10-29 14:39:39 +03:00 | | | target = self.root / 'instances/Fallback'
|
| | | self.assertEqual((target / 'terminal64.exe').read_bytes(), (reference / 'terminal64.exe').read_bytes())
|
| | | self.assertEqual((target / 'MetaEditor64.exe').read_bytes(), (reference / 'MetaEditor64.exe').read_bytes())
|
| | | self.assertEqual((target / 'config/servers.dat').read_bytes(), b'test-server-directory')
|
| | | self.assertEqual((target / SERVICE_REL).read_bytes(), b'compiled-service')
|
| | |
|
| | | def test_missing_servers_file_uses_reference_directory(self):
|
| | | reference = self.root / 'reference'
|
| | | (reference / 'config').mkdir(parents=True)
|
| | | (reference / 'config/servers.dat').write_bytes(b'reference-servers')
|
| | | (self.template / 'config/servers.dat').unlink()
|
| | | with patch.object(self.manager, 'reference_terminal', return_value=reference), \
|
2026-10-31 06:48:02 +03:00 | | | patch('terminal_manager.compile_service') as compile_call:
|
2026-10-29 14:39:39 +03:00 | | | task = self.run_job('create', 'ReferenceServers')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | compile_call.assert_not_called()
|
| | | self.assertEqual((self.root / 'instances/ReferenceServers/config/servers.dat').read_bytes(),
|
| | | b'reference-servers')
|
| | |
|
| | | def test_compile_failure_does_not_create_instance_or_copy_old_service(self):
|
| | | reference = self.root / 'reference'
|
| | | output = reference / SERVICE_REL
|
| | | output.parent.mkdir(parents=True)
|
| | | output.write_bytes(b'old-service')
|
| | | (self.template / 'ServiceStateWriter.ex5').unlink()
|
| | | with patch.object(self.manager, 'reference_terminal', return_value=reference), \
|
2026-10-31 06:48:02 +03:00 | | | patch('terminal_manager.compile_service', return_value=False):
|
2026-10-29 14:39:39 +03:00 | | | task = self.run_job('create', 'CompileFailed')
|
| | | self.assertEqual(task['status'], 'error', task)
|
| | | self.assertIn('скомпилировать', task['error'])
|
| | | self.assertFalse((self.root / 'instances/CompileFailed').exists())
|
| | | self.assertNotIn('CompileFailed', json.loads(self.path.read_text())['terminals'])
|
| | |
|
| | | def test_explicit_service_path_takes_priority_without_compilation(self):
|
| | | explicit = self.root / 'explicit.ex5'
|
| | | explicit.write_bytes(b'explicit-service')
|
| | | self.manager.save_settings({'service_ex5': str(explicit)})
|
2026-10-31 06:48:02 +03:00 | | | with patch('terminal_manager.compile_service') as compile_call:
|
2026-10-29 14:39:39 +03:00 | | | task = self.run_job('create', 'ExplicitService')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | compile_call.assert_not_called()
|
| | | self.assertEqual((self.root / 'instances/ExplicitService' / SERVICE_REL).read_bytes(), b'explicit-service')
|
| | |
|
2026-10-31 06:48:02 +03:00 | | | def test_standalone_project_with_uploaded_files_creates_instance(self):
|
| | | project = self.root / 'standalone'
|
| | | self.prepare_project(project)
|
| | | with patch('terminal_manager.PROJECT_DIR', project), \
|
| | | patch('terminal_manager.compile_service') as compile_call:
|
| | | self.assertIsNone(self.manager.reference_terminal())
|
| | | self.assertTrue(all(file['exists'] for file in self.manager.settings()['files'].values()))
|
| | | task = self.run_job('create', 'Standalone')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | compile_call.assert_not_called()
|
| | |
|
| | | def test_standalone_project_compiles_current_source_with_uploaded_editor(self):
|
| | | project = self.root / 'standalone'
|
| | | self.prepare_project(project)
|
| | | (project / 'ServiceStateWriter.mq5').write_text('current source')
|
| | | (self.template / 'ServiceStateWriter.ex5').unlink()
|
| | |
|
| | | def compile_service(editor, source, output):
|
| | | self.assertEqual(editor, self.template / 'MetaEditor64.exe')
|
| | | self.assertEqual(source.read_text(), 'current source')
|
| | | self.assertEqual(output, project / 'ServiceStateWriter.ex5')
|
| | | output.write_bytes(b'current compiled service')
|
| | | return True
|
| | |
|
| | | with patch('terminal_manager.PROJECT_DIR', project), \
|
| | | patch('terminal_manager.compile_service', side_effect=compile_service):
|
| | | task = self.run_job('create', 'CompiledStandalone')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | self.assertEqual((self.root / 'instances/CompiledStandalone' / SERVICE_REL).read_bytes(),
|
| | | b'current compiled service')
|
| | |
|
| | | def test_standalone_missing_files_shows_settings_and_reports_missing_asset(self):
|
| | | # Родительская папка с похожим EXE не является установкой MT5.
|
| | | (self.root / 'terminal64.exe').write_bytes(b'unrelated executable')
|
| | | for path in self.template.rglob('*'):
|
| | | if path.is_file():
|
| | | path.unlink()
|
| | | with patch('terminal_manager.PROJECT_DIR', self.root / 'standalone'):
|
| | | self.assertIsNone(self.manager.reference_terminal())
|
| | | self.assertFalse(any(file['exists'] for file in self.manager.settings()['files'].values()))
|
| | | task = self.run_job('create', 'MissingAssets')
|
| | | self.assertEqual(task['status'], 'error')
|
| | | self.assertIn('terminal64.exe', task['error'])
|
| | | self.assertFalse((self.root / 'instances/MissingAssets').exists())
|
| | |
|
| | | def test_explicit_installation_is_resolved_relative_to_config_and_used(self):
|
| | | installation = self.root / 'installed-terminal'
|
| | | installation.mkdir()
|
| | | (installation / 'terminal64.exe').write_bytes(b'configured terminal')
|
| | | (self.template / 'terminal64.exe').unlink()
|
| | | self.manager.save_settings({'mt5_installation': 'installed-terminal'})
|
| | | self.prepare_project(self.root / 'standalone')
|
| | | with patch('terminal_manager.PROJECT_DIR', self.root / 'standalone'):
|
| | | self.assertEqual(self.manager.reference_terminal(), installation)
|
| | | task = self.run_job('create', 'ConfiguredSource')
|
| | | self.assertEqual(task['status'], 'done', task)
|
| | | self.assertEqual((self.root / 'instances/ConfiguredSource/terminal64.exe').read_bytes(),
|
| | | b'configured terminal')
|
| | |
|
2026-10-29 14:39:39 +03:00 | | | def test_failed_creation_rolls_back_folder_and_config(self):
|
| | | self.manager.fail_wait = True
|
| | | task = self.run_job('create', 'Failed', {'login': 123, 'server': 'Broker', 'password': 'secret'})
|
| | | self.assertEqual(task['status'], 'error')
|
| | | self.assertNotIn('Failed', self.manager.store.data['terminals'])
|
| | | self.assertNotIn('Failed', json.loads(self.path.read_text())['terminals'])
|
| | | self.assertFalse((self.root / 'instances/Failed').exists())
|
| | | self.assertNotIn('Failed', self.manager.running)
|
| | |
|
| | | def test_occupied_folder_preserved(self):
|
| | | target = self.root / 'instances/Occupied'
|
| | | target.mkdir(parents=True)
|
| | | (target / 'keep.txt').write_text('existing data')
|
| | | self.assertEqual(self.run_job('create', 'occupied')['status'], 'error')
|
| | | self.assertEqual((target / 'keep.txt').read_text(), 'existing data')
|
| | |
|
| | | def test_password_change_reauthenticates_existing_terminal(self):
|
| | | original = {'login': 123, 'server': 'Broker', 'password': 'old-secret'}
|
| | | self.assertEqual(self.run_job('create', 'Account', original)['status'], 'done')
|
2026-10-31 07:26:44 +03:00 | | | target = Path(self.manager.control.instance_path('Account')).parent
|
2026-10-29 14:39:39 +03:00 | | | (target / 'keep.txt').write_text('history')
|
| | | changed = dict(original, password='new-secret')
|
| | | self.assertEqual(self.run_job('account', 'Account', changed)['status'], 'done')
|
| | | self.assertEqual(self.manager.saved_accounts['Account'][2], 'new-secret')
|
| | | self.assertEqual((target / 'keep.txt').read_text(), 'history')
|
| | |
|
| | | def test_existing_instance_kept_after_account_error(self):
|
| | | self.assertEqual(self.run_job('create', 'Existing')['status'], 'done')
|
| | | self.manager.fail_wait = True
|
| | | task = self.run_job('account', 'Existing', {'login': 123, 'server': 'Broker', 'password': 'secret'})
|
| | | self.assertEqual(task['status'], 'error')
|
| | | self.assertTrue((self.root / 'instances/Existing').exists())
|
| | | self.assertIn('Existing', self.manager.store.data['terminals'])
|
| | |
|
| | | def test_atomic_config_write_failure_preserves_original(self):
|
| | | original = self.path.read_bytes()
|
| | | changed = copy.deepcopy(self.manager.store.data)
|
| | | changed['disk_warning_mb'] = 100
|
| | | with patch('config.os.replace', side_effect=OSError('simulated write failure')):
|
| | | with self.assertRaises(OSError):
|
| | | self.manager.store.save(changed)
|
| | | self.assertEqual(self.path.read_bytes(), original)
|
| | | self.assertEqual(self.manager.store.data['disk_warning_mb'], 1024)
|
| | | self.assertFalse(list(self.root.glob('*.tmp')))
|
| | |
|
| | | def test_account_defaults_preserve_options_and_omit_password(self):
|
| | | directory = self.root / 'prepared'
|
| | | (directory / 'config').mkdir(parents=True)
|
| | | path = directory / 'config/common.ini'
|
| | | path.write_text('[Common]\nProxyEnable=1\nPassword=old-secret\n[Charts]\nMaxBars=20000\n', encoding='utf-16')
|
| | | self.manager.prepare_account_defaults(directory, (123, 'Broker-Demo', 'new-secret'))
|
| | | text = path.read_text(encoding='utf-16')
|
| | | for expected in ('Login=123', 'Server=Broker-Demo', 'KeepPrivate=1', 'ProxyEnable=1', '[Charts]\nMaxBars=20000'):
|
| | | self.assertIn(expected, text)
|
| | | self.assertNotIn('Password=', text)
|
| | | self.assertNotIn('secret', text)
|
| | |
|
| | | def test_upload_does_not_change_existing_server_directory(self):
|
| | | self.assertEqual(self.run_job('create', 'Existing')['status'], 'done')
|
| | | target = self.root / 'instances/Existing/config/servers.dat'
|
| | | original = target.read_bytes()
|
| | | incoming = self.root / 'new.dat'
|
| | | incoming.write_bytes(b'updated server directory')
|
| | | self.manager.install_upload('servers', incoming)
|
| | | self.assertEqual(target.read_bytes(), original)
|
| | | self.assertEqual((self.template / 'config/servers.dat').read_bytes(), incoming.read_bytes())
|
| | |
|
2026-10-31 07:26:44 +03:00 | | | def test_changed_root_updates_existing_paths_without_moving_directories(self):
|
2026-10-29 14:39:39 +03:00 | | | self.assertEqual(self.run_job('create', 'Existing')['status'], 'done')
|
| | | path = self.manager.control.instance_path('Existing')
|
2026-10-31 07:26:44 +03:00 | | | self.manager.verified_accounts['Existing'] = (123, 'Broker', 'secret')
|
2026-10-29 14:39:39 +03:00 | | | self.manager.save_settings({'mt5_folder': str(self.root / 'another-root')})
|
2026-10-31 07:26:44 +03:00 | | | expected = str(self.root / 'another-root/Existing/terminal64.exe')
|
| | | self.assertEqual(self.manager.control.instance_path('Existing'), expected)
|
| | | self.assertEqual(self.manager.settings()['config']['terminals']['Existing']['path'],
|
| | | str(self.root / 'another-root/Existing'))
|
| | | self.assertTrue(Path(path).is_file())
|
| | | self.assertFalse((self.root / 'another-root').exists())
|
| | | self.assertFalse(self.manager.verified_accounts)
|
| | | self.assertNotIn('path', json.loads(self.path.read_text())['terminals']['Existing'])
|
| | |
|
| | | def test_legacy_config_paths_are_ignored_and_removed_on_save(self):
|
| | | original = json.loads(self.path.read_text())
|
| | | original['terminals']['Existing'] = {'path': str(self.root / 'old-location'),
|
| | | 'login': 123, 'server': 'Broker', 'password': 'secret'}
|
| | | self.path.write_text(json.dumps(original), encoding='utf-8')
|
| | | store = ConfigStore(self.path)
|
| | | self.assertNotIn('path', store.data['terminals']['Existing'])
|
| | | control = MT5_Control(original['terminals'], store.data['mt5_folder'], store.data['database'])
|
| | | self.assertEqual(control.instance_path('Existing'),
|
| | | str(self.root / 'instances/Existing/terminal64.exe'))
|
| | | store.save(store.data)
|
| | | saved = json.loads(self.path.read_text())['terminals']['Existing']
|
| | | self.assertNotIn('path', saved)
|
| | | self.assertEqual(saved['password'], 'secret')
|
| | |
|
| | | def test_configuration_rejects_instance_names_outside_base_folder(self):
|
| | | for name in ('../outside', 'C:/elsewhere', 'nested/child', '..'):
|
| | | original = json.loads(self.path.read_text())
|
| | | original['terminals'] = {name: {}}
|
| | | self.path.write_text(json.dumps(original), encoding='utf-8')
|
| | | with self.assertRaises(ValueError):
|
| | | ConfigStore(self.path)
|
| | |
|
| | | def test_process_detection_follows_changed_base_folder(self):
|
| | | original = {'Existing': {'path': str(self.root / 'old-base/Existing')}}
|
| | | new_base = self.root / 'new-base'
|
| | | control = MT5_Control(original, str(new_base), str(self.root / 'state.sqlite'))
|
| | | old_process = type('Process', (), {'info': {'pid': 111, 'exe': str(self.root / 'old-base/Existing/terminal64.exe')}})()
|
| | | new_process = type('Process', (), {'info': {'pid': 222, 'exe': str(new_base / 'Existing/terminal64.exe')}})()
|
| | | with patch('mt5_control.psutil.process_iter', return_value=[old_process]):
|
| | | control.find_instances()
|
| | | self.assertEqual(control.instances['Existing']['pid'], 0)
|
| | | with patch('mt5_control.psutil.process_iter', return_value=[old_process, new_process]):
|
| | | control.find_instances()
|
| | | self.assertEqual(control.instances['Existing']['pid'], 222)
|
| | |
|
| | | def test_state_from_old_base_is_not_used_for_terminal_in_new_base(self):
|
| | | control = MT5_Control({'Existing': {}}, str(self.root / 'new-base'), str(self.root / 'state.sqlite'))
|
| | | control.instances['Existing']['pid'] = 222
|
| | | row = {'terminal_path': str(self.root / 'old-base/Existing'), 'updated_at': time.time()}
|
| | | with patch.object(control, 'read_state', return_value=row):
|
| | | info = control.instance_info('Existing')
|
| | | self.assertEqual(info['status'], 'starting')
|
| | | self.assertNotIn('account', info)
|
2026-10-29 14:39:39 +03:00 | | |
|
| | | def test_wait_requires_fresh_matching_connected_account(self):
|
| | | self.assertEqual(self.run_job('create', 'Existing')['status'], 'done')
|
| | | path = str(self.root / 'instances/Existing')
|
| | | row = {'terminal_path': path, 'updated_at': time.time() - 100,
|
| | | 'connected': 1, 'login': 123, 'server': 'Broker'}
|
| | | with patch.object(self.manager.control, 'read_state', return_value=row), \
|
| | | patch.object(self.manager.closing, 'wait'), \
|
| | | patch('terminal_manager.time.monotonic', side_effect=[0, 0, 9999]):
|
| | | with self.assertRaises(RuntimeError):
|
| | | TerminalManager.wait_state(self.manager, 'Existing', time.time(), (123, 'Broker', 'secret'))
|
| | | row.update(updated_at=time.time() + 1, connected=0)
|
| | | with patch.object(self.manager.control, 'read_state', return_value=row), \
|
| | | patch.object(self.manager.closing, 'wait'), \
|
| | | patch('terminal_manager.time.monotonic', side_effect=[0, 0, 9999]):
|
| | | with self.assertRaises(RuntimeError):
|
| | | TerminalManager.wait_state(self.manager, 'Existing', time.time(), (123, 'Broker', 'secret'))
|
| | |
|
| | | def test_reject_bad_names_and_partial_new_account(self):
|
| | | for folder in ('../other', 'CON', 'name.', 'a/b', 'nul.txt', ' trailing '):
|
| | | with self.assertRaises(ValueError):
|
| | | validate_folder(folder)
|
| | | with self.assertRaises(ValueError):
|
| | | account_values({'login': 123, 'server': 'Broker'}, complete=True)
|
| | | with self.assertRaises(ValueError):
|
| | | account_values({'login': 123, 'server': 'Broker', 'password': 'secret\nKeepPrivate=0'})
|
| | |
|
| | |
|
| | | if __name__ == '__main__':
|
| | | unittest.main()
|