125 lines
5.9 KiB
Python
125 lines
5.9 KiB
Python
"""Redis configuration state-machine tests with no live Redis/server access."""
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
SOURCE = Path(__file__).resolve().parents[1]/'install-redis-persistence.py'
|
|
OLD = {'appendonly': 'no', 'appendfsync': 'always', 'maxmemory': '0', 'maxmemory-policy': 'allkeys-lru'}
|
|
WANTED = {'appendonly': 'yes', 'appendfsync': 'everysec', 'maxmemory': '268435456', 'maxmemory-policy': 'noeviction'}
|
|
|
|
|
|
@unittest.skipUnless(os.name == 'posix' and getattr(os, 'geteuid', lambda: -1)() == 0,
|
|
'Disposable root Linux container required')
|
|
class RedisPersistenceTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory(prefix='redis-persistence-test-')
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
script = SOURCE.read_text()
|
|
for prefix in ('/etc/', '/var/', '/run/'):
|
|
script = script.replace(prefix, str(self.root)+prefix)
|
|
self.module = importlib.util.module_from_spec(importlib.util.spec_from_loader('redis_persistence', loader=None))
|
|
exec(compile(script, str(SOURCE), 'exec'), self.module.__dict__)
|
|
for directory in ('etc/redis', 'var/lib/eusrr-deploy', 'var/lib/redis', 'run/lock'):
|
|
(self.root/directory).mkdir(parents=True, exist_ok=True)
|
|
(self.root/'etc/redis/redis.conf').write_text('original configuration\n')
|
|
self.state = dict(OLD)
|
|
self.calls = []
|
|
self.rewrites = 0
|
|
self.fail_stage = None
|
|
self.verification_failed = False
|
|
|
|
def fake_redis(self, *args):
|
|
self.calls.append(args)
|
|
if args == ('INFO', 'memory'):
|
|
return 'used_memory:' + ('220000000' if self.fail_stage == 'memory' else '4194304')
|
|
if args == ('INFO', 'persistence'):
|
|
status = 'err' if self.fail_stage in ('aof', 'rollback') else 'ok'
|
|
return f'aof_enabled:1\naof_rewrite_in_progress:0\naof_rewrite_scheduled:0\naof_last_bgrewrite_status:{status}\n'
|
|
if args[:2] == ('CONFIG', 'GET'):
|
|
if args[2] == 'bind':
|
|
return {'bind': '127.0.0.1 -::1'}
|
|
if args[2] == 'dir':
|
|
return {'dir': str(self.root/'var/lib/redis')}
|
|
if self.fail_stage == 'verification' and self.rewrites == 1 and not self.verification_failed:
|
|
self.verification_failed = True
|
|
return dict(self.state, appendfsync='unexpected')
|
|
return dict(self.state)
|
|
if args[:2] == ('CONFIG', 'SET'):
|
|
key, value = args[2:]
|
|
if self.fail_stage == 'set' and key == 'maxmemory' and value == WANTED[key]:
|
|
raise RuntimeError('Injected CONFIG SET failure')
|
|
if self.fail_stage == 'rollback' and key == 'appendonly' and value == OLD[key]:
|
|
raise RuntimeError('Injected rollback failure')
|
|
self.state[key] = value
|
|
return 'OK'
|
|
if args == ('CONFIG', 'REWRITE'):
|
|
self.rewrites += 1
|
|
return 'OK'
|
|
raise AssertionError(args)
|
|
|
|
def run_installer(self):
|
|
with patch.object(self.module, 'redis', side_effect=self.fake_redis), \
|
|
patch.object(self.module.time, 'sleep'), contextlib.redirect_stdout(io.StringIO()):
|
|
self.module.main()
|
|
|
|
def test_success_checks_aof_then_persists_and_verifies(self):
|
|
self.run_installer()
|
|
self.assertEqual(self.state, WANTED)
|
|
self.assertEqual(self.rewrites, 1)
|
|
self.assertLess(self.calls.index(('INFO', 'persistence')), self.calls.index(('CONFIG', 'REWRITE')))
|
|
self.assertFalse(any(args[0] in ('FLUSHALL', 'FLUSHDB', 'DEL', 'SHUTDOWN') for args in self.calls))
|
|
|
|
def test_set_aof_and_post_rewrite_verification_failures_all_restore(self):
|
|
for stage in ('set', 'aof', 'verification'):
|
|
with self.subTest(stage=stage):
|
|
self.fail_stage = stage
|
|
self.state = dict(OLD)
|
|
self.rewrites = 0
|
|
self.verification_failed = False
|
|
for directory in (self.root/'var/lib/eusrr-deploy').iterdir():
|
|
shutil.rmtree(directory)
|
|
with self.assertRaises(RuntimeError):
|
|
self.run_installer()
|
|
self.assertEqual(self.state, OLD)
|
|
self.assertGreaterEqual(self.rewrites, 1)
|
|
|
|
def test_rollback_attempts_remaining_settings_and_reports_incomplete(self):
|
|
self.fail_stage = 'rollback'
|
|
with self.assertRaisesRegex(RuntimeError, 'Redis recovery incomplete'):
|
|
self.run_installer()
|
|
for key in ('appendfsync', 'maxmemory', 'maxmemory-policy'):
|
|
self.assertEqual(self.state[key], OLD[key])
|
|
self.assertIn(('CONFIG', 'REWRITE'), self.calls)
|
|
|
|
def test_high_memory_fails_before_mutation(self):
|
|
self.fail_stage = 'memory'
|
|
with self.assertRaisesRegex(SystemExit, 'too close'):
|
|
self.run_installer()
|
|
self.assertFalse(any(args[:2] == ('CONFIG', 'SET') for args in self.calls))
|
|
|
|
def test_low_disk_fails_before_mutation(self):
|
|
with patch.object(self.module.shutil, 'disk_usage', return_value=shutil._ntuple_diskusage(10000000, 9000000, 1000000)):
|
|
with self.assertRaisesRegex(SystemExit, 'Insufficient free space'):
|
|
self.run_installer()
|
|
self.assertFalse(any(args[:2] == ('CONFIG', 'SET') for args in self.calls))
|
|
|
|
def test_cli_raw_and_json_info_formats(self):
|
|
for output in ('used_memory:123\r\n', json.dumps('used_memory:123\r\n')):
|
|
with self.subTest(output=output), patch.object(self.module.subprocess, 'run',
|
|
return_value=subprocess.CompletedProcess(['redis-cli'], 0, stdout=output)):
|
|
self.assertEqual(self.module.redis('INFO', 'memory'), 'used_memory:123\r\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|