Smail/test/deployment/test_runtime_sql_switch_fault.py
smail 079ebee495
Some checks are pending
Security Checks / security (push) Waiting to run
补充SQL切换联合故障验收工具并校准系统调用边界
2026-09-22 00:58:02 +08:00

86 lines
4.9 KiB
Python

"""联合故障工具的范围、阶段与失败判定回归,不连接真实服务。"""
import http.client
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import Mock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools'))
import runtime_sql_switch_fault as fault
from runtime_sql import SQLFixture
class RuntimeSQLSwitchFaultTest(unittest.TestCase):
def test_every_thread_must_be_stopped(self):
task = Mock()
for state, expected in (('S', False), ('R', False), ('T', True), ('t', True)):
task.__truediv__ = Mock(return_value=Mock(read_text=Mock(return_value='State:\t' + state + ' (state)\n')))
with patch.object(Path, 'iterdir', return_value=iter([task])):
self.assertEqual(fault.stopped_tasks('123'), expected)
def test_formal_scope_rejected_before_process_or_database_access(self):
with patch.object(fault.subprocess, 'Popen') as process, patch.object(fault, 'service_state') as state:
with self.assertRaises(AssertionError):
fault.verify_switch_fault(Path('/opt/smail'), 'smail.service', 'unused', {},
SQLFixture('mysql'), SQLFixture('mysql'), 'before-root')
process.assert_not_called()
state.assert_not_called()
def test_boundary_requires_active_directory_phase_and_expected_database(self):
source, target = SQLFixture('mysql'), SQLFixture('mysql')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
archive = root / 'data/transaction/one'
archive.mkdir(parents=True)
(root / 'config').mkdir()
trace = root / 'trace.log'
manifest = archive / 'manifest.json'
for phase, name in (('before-root', source.settings['name']), ('after-root', target.settings['name'])):
(root / 'config/config.json').write_text(json.dumps({'database': {'name': name}}))
manifest.write_text(json.dumps({'phase': 'sections_committed'}))
for path, expected in (('/opt/smail/data/transaction/one/candidate', False),
('/opt/smail/config', True), ('/opt/smail/config-other', False)):
trace.write_text('renameat(4<' + path + '>, ".config.json-a", 4<' + path + '>, "config.json"')
self.assertEqual(bool(fault.boundary(root, trace, phase, source, target)), expected)
trace.write_text('renameat(4</opt/smail/config>, ".config.json-a", 4</opt/smail/config>, "config.json"')
manifest.write_text(json.dumps({'phase': 'root_committed'}))
self.assertIsNone(fault.boundary(root, trace, phase, source, target))
def test_committed_requires_matching_nonempty_mail_and_preserved_history(self):
source, target = Mock(), Mock()
for mail, history in (([[], []], [['a'], ['a']]),
([['a'], ['b']], [['a'], ['a']]),
([['a'], ['a']], [[], []]),
([['a'], ['a']], [['a'], ['b']])):
with patch.object(fault, 'table_rows', side_effect=mail), \
patch.object(fault, 'history_rows', side_effect=history):
with self.assertRaises(AssertionError):
fault.assert_committed(source, target)
with patch.object(fault, 'table_rows', side_effect=[['mail'], ['mail']]), \
patch.object(fault, 'history_rows', side_effect=[['old'], ['old', 'new']]):
self.assertEqual(fault.assert_committed(source, target)['committed_history_rows'], 2)
def test_timeout_and_regular_response_cannot_pass_as_killed_process(self):
for outcome in (None, TimeoutError(), http.client.RemoteDisconnected(), ConnectionResetError()):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / 'instance'
client = Mock()
client.success.return_value = {'migration': {'id': 'test-id'}, 'token': 'not-for-logs'}
client.request.side_effect = outcome
with patch.object(fault, 'login', return_value=(client, None)), \
patch.object(fault, 'clear_pending_restart', return_value=1), \
patch.object(fault, 'wait_until'):
if isinstance(outcome, (http.client.RemoteDisconnected, ConnectionResetError)):
self.assertTrue(fault.prepare(root, 'unused', SQLFixture('mysql'))['request_disconnected'])
else:
with self.assertRaises((AssertionError, TimeoutError)):
fault.prepare(root, 'unused', SQLFixture('mysql'))
self.assertEqual((root.parent / 'switch-ready').read_bytes(), b'')
if __name__ == '__main__':
unittest.main()